diff --git a/.claude/settings.local.json b/.claude/settings.local.json
deleted file mode 100644
index d0df7d3..0000000
--- a/.claude/settings.local.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "permissions": {
- "allow": [
- "Bash(tree:*)"
- ]
- }
-}
diff --git a/.gitignore b/.gitignore
index e23a945..e756a88 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,8 @@
[Uu]ser[Ss]ettings/
.vs
-.vs/*
\ No newline at end of file
+.vs/*
+.vscode
+.claude
+Assets/_Recovery
+Assets/_Recovery.meta
diff --git a/Assembly-CSharp-Editor.csproj b/Assembly-CSharp-Editor.csproj
index 9241097..478f335 100644
--- a/Assembly-CSharp-Editor.csproj
+++ b/Assembly-CSharp-Editor.csproj
@@ -43,7 +43,6 @@
6000.3.5f2
-
@@ -621,6 +620,10 @@
C:\Program Files\Unity\Hub\Editor\6000.3.5f2\Editor\Data\PlaybackEngines\WindowsStandaloneSupport\UnityEditor.WindowsStandalone.Extensions.dll
False
+
+ Library\PackageCache\com.unity.services.wire@9a73acde80cc\Plugins\websocket-sharp.dll
+ False
+
Library\PackageCache\com.unity.visualscripting@191c0d7e3b69\Editor\VisualScripting.Core\Dependencies\YamlDotNet\Unity.VisualScripting.YamlDotNet.dll
False
@@ -1213,10 +1216,22 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.MetricTestData.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.QoS.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Services.Multiplayer.Editor.Shared.dll
+ False
+
Library\ScriptAssemblies\Unity.Rendering.LightTransport.Editor.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Core.dll
+ False
+
Library\ScriptAssemblies\Unity.Netcode.Runtime.dll
False
@@ -1245,14 +1260,14 @@
Library\ScriptAssemblies\PPv2URPConverters.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Common.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.AI.Navigation.Updater.dll
False
+
+ Library\ScriptAssemblies\Unity.2D.Common.Editor.dll
+ False
+
Library\ScriptAssemblies\Unity.AI.Navigation.Editor.dll
False
@@ -1277,6 +1292,10 @@
Library\ScriptAssemblies\Unity.RenderPipelines.Universal.2D.Runtime.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Core.Components.dll
+ False
+
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Configuration.dll
False
@@ -1289,8 +1308,12 @@
Library\ScriptAssemblies\Unity.2D.Aseprite.Common.dll
False
-
- Library\ScriptAssemblies\Unity.Rider.Editor.dll
+
+ Library\ScriptAssemblies\Unity.Cursor.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Services.Authentication.dll
False
@@ -1301,10 +1324,22 @@
Library\ScriptAssemblies\Unity.2D.Aseprite.Editor.dll
False
+
+ Library\ScriptAssemblies\Unity.Rider.Editor.dll
+ False
+
Library\ScriptAssemblies\Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Authentication.Editor.Shared.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Services.Multiplayer.Editor.Matchmaker.Authoring.dll
+ False
+
Library\ScriptAssemblies\Unity.VisualScripting.Flow.dll
False
@@ -1317,6 +1352,10 @@
Library\ScriptAssemblies\Unity.2D.Tilemap.Extras.Editor.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Authentication.PlayerAccounts.dll
+ False
+
Library\ScriptAssemblies\Unity.VisualStudio.Editor.dll
False
@@ -1349,6 +1388,14 @@
Library\ScriptAssemblies\UnityEngine.UI.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Core.Environments.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Services.Multiplayer.dll
+ False
+
Library\ScriptAssemblies\com.unity.multiplayer.tools.window.dll
False
@@ -1485,10 +1532,18 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetVis.Editor.Visualization.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Core.Analytics.dll
+ False
+
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Editor.dll
False
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Center.NetcodeForGameObjectsExample.dll
+ False
+
Library\ScriptAssemblies\Unity.Multiplayer.Tools.DependencyInjection.UIElements.dll
False
@@ -1553,6 +1608,10 @@
Library\ScriptAssemblies\Unity.Multiplayer.Center.Common.dll
False
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Center.Integrations.dll
+ False
+
Library\ScriptAssemblies\Unity.Burst.dll
False
diff --git a/Assembly-CSharp.csproj b/Assembly-CSharp.csproj
index 3ee2004..1800786 100644
--- a/Assembly-CSharp.csproj
+++ b/Assembly-CSharp.csproj
@@ -43,16 +43,13 @@
6000.3.5f2
-
-
-
@@ -64,10 +61,12 @@
+
+
@@ -81,9 +80,12 @@
+
+
+
+
-
@@ -93,15 +95,18 @@
+
+
+
@@ -687,6 +692,10 @@
C:\Program Files\Unity\Hub\Editor\6000.3.5f2\Editor\Data\Managed\UnityEngine\UnityEditor.XRModule.dll
False
+
+ Library\PackageCache\com.unity.services.wire@9a73acde80cc\Plugins\websocket-sharp.dll
+ False
+
Library\PackageCache\com.unity.collections@aea9d3bd5e19\Unity.Collections.LowLevel.ILSupport\Unity.Collections.LowLevel.ILSupport.dll
False
@@ -1231,10 +1240,22 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.MetricTestData.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.QoS.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Services.Multiplayer.Editor.Shared.dll
+ False
+
Library\ScriptAssemblies\Unity.Rendering.LightTransport.Editor.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Core.dll
+ False
+
Library\ScriptAssemblies\Unity.Netcode.Runtime.dll
False
@@ -1263,14 +1284,14 @@
Library\ScriptAssemblies\PPv2URPConverters.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Common.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.AI.Navigation.Updater.dll
False
+
+ Library\ScriptAssemblies\Unity.2D.Common.Editor.dll
+ False
+
Library\ScriptAssemblies\Unity.AI.Navigation.Editor.dll
False
@@ -1295,6 +1316,10 @@
Library\ScriptAssemblies\Unity.RenderPipelines.Universal.2D.Runtime.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Core.Components.dll
+ False
+
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Configuration.dll
False
@@ -1307,8 +1332,12 @@
Library\ScriptAssemblies\Unity.2D.Aseprite.Common.dll
False
-
- Library\ScriptAssemblies\Unity.Rider.Editor.dll
+
+ Library\ScriptAssemblies\Unity.Cursor.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Services.Authentication.dll
False
@@ -1319,10 +1348,22 @@
Library\ScriptAssemblies\Unity.2D.Aseprite.Editor.dll
False
+
+ Library\ScriptAssemblies\Unity.Rider.Editor.dll
+ False
+
Library\ScriptAssemblies\Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Authentication.Editor.Shared.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Services.Multiplayer.Editor.Matchmaker.Authoring.dll
+ False
+
Library\ScriptAssemblies\Unity.VisualScripting.Flow.dll
False
@@ -1335,6 +1376,10 @@
Library\ScriptAssemblies\Unity.2D.Tilemap.Extras.Editor.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Authentication.PlayerAccounts.dll
+ False
+
Library\ScriptAssemblies\Unity.VisualStudio.Editor.dll
False
@@ -1367,6 +1412,14 @@
Library\ScriptAssemblies\UnityEngine.UI.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Core.Environments.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Services.Multiplayer.dll
+ False
+
Library\ScriptAssemblies\com.unity.multiplayer.tools.window.dll
False
@@ -1503,10 +1556,18 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetVis.Editor.Visualization.dll
False
+
+ Library\ScriptAssemblies\Unity.Services.Core.Analytics.dll
+ False
+
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Editor.dll
False
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Center.NetcodeForGameObjectsExample.dll
+ False
+
Library\ScriptAssemblies\Unity.Multiplayer.Tools.DependencyInjection.UIElements.dll
False
@@ -1571,6 +1632,10 @@
Library\ScriptAssemblies\Unity.Multiplayer.Center.Common.dll
False
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Center.Integrations.dll
+ False
+
Library\ScriptAssemblies\Unity.Burst.dll
False
diff --git a/Assets/Animations/Attack.anim b/Assets/Animations/Attack.anim
index e4caff3..c7d3275 100644
--- a/Assets/Animations/Attack.anim
+++ b/Assets/Animations/Attack.anim
@@ -27137,13 +27137,20 @@ AnimationClip:
floatParameter: 0
intParameter: 0
messageOptions: 0
- - time: 1.0333333
+ - time: 1
functionName: OnUnequipWeapon
data: handslot.r
objectReferenceParameter: {fileID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
+ - time: 1.0333333
+ functionName: OnAttackHit
+ data:
+ objectReferenceParameter: {fileID: 0}
+ floatParameter: 0
+ intParameter: 0
+ messageOptions: 0
- time: 1.0666667
functionName: OnAttackComplete
data:
diff --git a/Assets/Prefabs/Bat.prefab b/Assets/Prefabs/Bat.prefab
new file mode 100644
index 0000000..e470002
--- /dev/null
+++ b/Assets/Prefabs/Bat.prefab
@@ -0,0 +1,124 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &6430526095172430446
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 5247455493693100088}
+ - component: {fileID: 1386822053491278300}
+ - component: {fileID: 337360336842274253}
+ m_Layer: 0
+ m_Name: GameObject
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &5247455493693100088
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6430526095172430446}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 6825883197719469110}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &1386822053491278300
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6430526095172430446}
+ m_Mesh: {fileID: 716655249603938094, guid: 1261145a64d4f3e43bee728a02c1b5e3, type: 3}
+--- !u!23 &337360336842274253
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6430526095172430446}
+ 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: d64c307f1b4197c44970c29f9845c245, 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!1 &9113353226622644240
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 6825883197719469110}
+ m_Layer: 0
+ m_Name: Bat
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &6825883197719469110
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 9113353226622644240}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -5.09173, y: 2, z: -37.867}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 5247455493693100088}
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
diff --git a/Assets/Prefabs/Bat.prefab.meta b/Assets/Prefabs/Bat.prefab.meta
new file mode 100644
index 0000000..05500b3
--- /dev/null
+++ b/Assets/Prefabs/Bat.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 66fa238e7512e1547bf2ef4b62d76afd
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Prefabs/Player.prefab b/Assets/Prefabs/Player.prefab
index 4f292c3..705d609 100644
--- a/Assets/Prefabs/Player.prefab
+++ b/Assets/Prefabs/Player.prefab
@@ -10,16 +10,18 @@ GameObject:
m_Component:
- component: {fileID: 5887522270574905679}
- component: {fileID: 2636831972010436653}
- - component: {fileID: 3792365921352178844}
+ - component: {fileID: 5989504606015899400}
- component: {fileID: 1698609800605343773}
- component: {fileID: 3007098678582223509}
- component: {fileID: 1883169379180791275}
- component: {fileID: 8729870597719024730}
- component: {fileID: 5217638038410020423}
- - component: {fileID: 9062880697264624749}
+ - component: {fileID: 2753625045547947614}
- component: {fileID: 6066313428661204362}
- - component: {fileID: 2443072964133329520}
- component: {fileID: 2148255267416253297}
+ - component: {fileID: 7256821725794344131}
+ - component: {fileID: 5045194683846919184}
+ - component: {fileID: 7246316046087380146}
m_Layer: 9
m_Name: Player
m_TagString: Untagged
@@ -55,7 +57,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
- GlobalObjectIdHash: 4211758632
+ GlobalObjectIdHash: 1360081626
InScenePlacedSourceGlobalObjectIdHash: 4211758632
DeferredDespawnTick: 0
Ownership: 1
@@ -68,7 +70,7 @@ MonoBehaviour:
AutoObjectParentSync: 1
SyncOwnerTransformWhenParented: 1
AllowOwnerToParent: 0
---- !u!114 &3792365921352178844
+--- !u!114 &5989504606015899400
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -77,15 +79,15 @@ MonoBehaviour:
m_GameObject: {fileID: 1314983689436087486}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 9e8f7d6c5b4a3d2e1f0a9b8c7d6e5f4a, type: 3}
+ m_Script: {fileID: 11500000, guid: 19b1385c0dcb77240b2cfb3c6b10f717, type: 3}
m_Name:
- m_EditorClassIdentifier: Assembly-CSharp::NetworkPlayerController
+ m_EditorClassIdentifier: Assembly-CSharp::Northbound.NetworkPlayerController
ShowTopMostFoldoutHeaderGroup: 1
moveSpeed: 5
rotationSpeed: 10
initialTeam: 1
maxHealth: 100
- showHealthBar: 1
+ showHealthBar: 0
damageEffectPrefab: {fileID: 0}
deathEffectPrefab: {fileID: 0}
--- !u!95 &1698609800605343773
@@ -175,7 +177,7 @@ MonoBehaviour:
interactableLayer:
serializedVersion: 2
m_Bits: 128
- rayOrigin: {fileID: 0}
+ rayOrigin: {fileID: 6366268612314379957}
useForwardDirection: 1
playAnimations: 1
useAnimationEvents: 1
@@ -196,9 +198,9 @@ MonoBehaviour:
m_EditorClassIdentifier: Assembly-CSharp::Northbound.PlayerActionSystem
ShowTopMostFoldoutHeaderGroup: 1
actionComponents:
- - {fileID: 9062880697264624749}
+ - {fileID: 2753625045547947614}
playAnimations: 1
---- !u!114 &9062880697264624749
+--- !u!114 &2753625045547947614
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -207,23 +209,23 @@ MonoBehaviour:
m_GameObject: {fileID: 1314983689436087486}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 66ec3984614d8a64b8eae821376d038d, type: 3}
+ m_Script: {fileID: 11500000, guid: c467f66382052bc429affc5cb01d17db, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Northbound.AttackAction
ShowTopMostFoldoutHeaderGroup: 1
- attackRange: 3
- attackDamage: 100
+ attackRange: 2
+ attackDamage: 10
attackCooldown: 0.5
attackableLayer:
serializedVersion: 2
- m_Bits: 4294967295
+ m_Bits: 256
attackAnimationTrigger: Attack
useAnimationEvents: 1
blockDuringAnimation: 1
useEquipment: 1
equipmentData:
socketName: handslot.r
- equipmentPrefab: {fileID: 919132149155446097, guid: 1261145a64d4f3e43bee728a02c1b5e3, type: 3}
+ equipmentPrefab: {fileID: 9113353226622644240, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
attachOnStart: 1
detachOnEnd: 1
keepEquipped: 0
@@ -243,24 +245,14 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: ac908541bf903c745a1794d409a5f048, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Northbound.EquipmentSocket
+ ShowTopMostFoldoutHeaderGroup: 1
sockets:
- socketName: handslot.r
socketTransform: {fileID: 2844947653216056832}
currentEquipment: {fileID: 0}
---- !u!114 &2443072964133329520
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1314983689436087486}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 3c64072402b0a3f46a674eb73c5541ac, type: 3}
- m_Name:
- m_EditorClassIdentifier: Assembly-CSharp::Northbound.PlayerResourceInventory
- ShowTopMostFoldoutHeaderGroup: 1
- maxResourceCapacity: 50
+ equipmentPrefabs:
+ - {fileID: 9113353226622644240, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ - {fileID: 7492862814627084760, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
--- !u!114 &2148255267416253297
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -275,6 +267,98 @@ MonoBehaviour:
m_EditorClassIdentifier: Assembly-CSharp::Northbound.PlayerVisionProvider
ShowTopMostFoldoutHeaderGroup: 1
visionRange: 10
+--- !u!114 &7256821725794344131
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1314983689436087486}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: e96cb6065543e43c4a752faaa1468eb1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkTransform
+ ShowTopMostFoldoutHeaderGroup: 1
+ NetworkTransformExpanded: 0
+ AutoOwnerAuthorityTickOffset: 1
+ PositionInterpolationType: 0
+ RotationInterpolationType: 0
+ ScaleInterpolationType: 0
+ PositionLerpSmoothing: 1
+ PositionMaxInterpolationTime: 0.1
+ RotationLerpSmoothing: 1
+ RotationMaxInterpolationTime: 0.1
+ ScaleLerpSmoothing: 1
+ ScaleMaxInterpolationTime: 0.1
+ AuthorityMode: 0
+ TickSyncChildren: 0
+ UseUnreliableDeltas: 0
+ SyncPositionX: 1
+ SyncPositionY: 1
+ SyncPositionZ: 1
+ SyncRotAngleX: 1
+ SyncRotAngleY: 1
+ SyncRotAngleZ: 1
+ SyncScaleX: 1
+ SyncScaleY: 1
+ SyncScaleZ: 1
+ PositionThreshold: 0.001
+ RotAngleThreshold: 0.01
+ ScaleThreshold: 0.01
+ UseQuaternionSynchronization: 0
+ UseQuaternionCompression: 0
+ UseHalfFloatPrecision: 0
+ InLocalSpace: 0
+ SwitchTransformSpaceWhenParented: 0
+ Interpolate: 1
+ SlerpPosition: 0
+--- !u!114 &5045194683846919184
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1314983689436087486}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: e8d0727d5ae3244e3b569694d3912374, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkAnimator
+ ShowTopMostFoldoutHeaderGroup: 1
+ NetworkAnimatorExpanded: 0
+ AuthorityMode: 0
+ m_Animator: {fileID: 1698609800605343773}
+ TransitionStateInfoList: []
+ AnimatorParameterEntries:
+ ParameterEntries:
+ - name: MoveSpeed
+ NameHash: 526065662
+ Synchronize: 1
+ ParameterType: 1
+ - name: Mining
+ NameHash: 577859424
+ Synchronize: 1
+ ParameterType: 9
+ - name: Attack
+ NameHash: 1080829965
+ Synchronize: 1
+ ParameterType: 9
+ AnimatorParametersExpanded: 0
+--- !u!114 &7246316046087380146
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1314983689436087486}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 90231c209cbef84469511de397004be9, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::Northbound.PlayerInventory
+ ShowTopMostFoldoutHeaderGroup: 1
+ maxResourceCapacity: 100
--- !u!1001 &1445453803682481668
PrefabInstance:
m_ObjectHideFlags: 0
@@ -502,3 +586,8 @@ Transform:
m_CorrespondingSourceObject: {fileID: -5515783359193845756, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
m_PrefabInstance: {fileID: 1445453803682481668}
m_PrefabAsset: {fileID: 0}
+--- !u!4 &6366268612314379957 stripped
+Transform:
+ m_CorrespondingSourceObject: {fileID: 5500811601672728753, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ m_PrefabInstance: {fileID: 1445453803682481668}
+ m_PrefabAsset: {fileID: 0}
diff --git a/Assets/Prefabs/Wall.prefab b/Assets/Prefabs/Wall.prefab
index 3655c3f..5d12691 100644
--- a/Assets/Prefabs/Wall.prefab
+++ b/Assets/Prefabs/Wall.prefab
@@ -86,7 +86,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
- GlobalObjectIdHash: 1061286994
+ GlobalObjectIdHash: 1574233340
InScenePlacedSourceGlobalObjectIdHash: 1061286994
DeferredDespawnTick: 0
Ownership: 1
@@ -112,17 +112,18 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Northbound.Building
ShowTopMostFoldoutHeaderGroup: 1
+ maxHealth: 100
+ showHealthBar: 1
+ damageEffectPrefab: {fileID: 0}
+ destroyEffectPrefab: {fileID: 0}
+ effectSpawnPoint: {fileID: 0}
buildingData: {fileID: 11400000, guid: 0e495d169ee3bce449f4b1aea83d6818, type: 2}
gridPosition: {x: 0, y: 0, z: 0}
rotation: 0
initialTeam: 1
initialOwnerId: 1
useInitialOwner: 1
- 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!114 &9023294375343742146
@@ -140,6 +141,9 @@ MonoBehaviour:
showInExploredAreas: 1
updateInterval: 0.2
renderers: []
+ enableDistantVisibility: 1
+ heightVisibilityMultiplier: 2
+ minHeightForDistantVisibility: 3
useExploredMaterial: 0
exploredMaterial: {fileID: 0}
--- !u!1001 &8926581783111832504
diff --git a/Assets/Resources.meta b/Assets/Resources.meta
new file mode 100644
index 0000000..5d91ef4
--- /dev/null
+++ b/Assets/Resources.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0aef2ea1f5e3aed4d839d26090fcb09c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Prefabs.meta b/Assets/Resources/Prefabs.meta
new file mode 100644
index 0000000..454c45f
--- /dev/null
+++ b/Assets/Resources/Prefabs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3d69632731200bd4a86a369c95ee485b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Prefabs/Bat.prefab b/Assets/Resources/Prefabs/Bat.prefab
new file mode 100644
index 0000000..0a46575
--- /dev/null
+++ b/Assets/Resources/Prefabs/Bat.prefab
@@ -0,0 +1,124 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &6430526095172430446
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 5247455493693100088}
+ - component: {fileID: 1386822053491278300}
+ - component: {fileID: 337360336842274253}
+ m_Layer: 0
+ m_Name: GameObject
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &5247455493693100088
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6430526095172430446}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 6825883197719469110}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &1386822053491278300
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6430526095172430446}
+ m_Mesh: {fileID: 716655249603938094, guid: 1261145a64d4f3e43bee728a02c1b5e3, type: 3}
+--- !u!23 &337360336842274253
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6430526095172430446}
+ 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: 726c1e9087356f74594664341c681f12, 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!1 &9113353226622644240
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 6825883197719469110}
+ m_Layer: 0
+ m_Name: Bat
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &6825883197719469110
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 9113353226622644240}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -5.09173, y: 2, z: -37.867}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 5247455493693100088}
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
diff --git a/Assets/Resources/Prefabs/Bat.prefab.meta b/Assets/Resources/Prefabs/Bat.prefab.meta
new file mode 100644
index 0000000..aea299c
--- /dev/null
+++ b/Assets/Resources/Prefabs/Bat.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 63b758cca5ef327449b0debcea47b006
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Prefabs/Pickaxe.prefab b/Assets/Resources/Prefabs/Pickaxe.prefab
new file mode 100644
index 0000000..87cb517
--- /dev/null
+++ b/Assets/Resources/Prefabs/Pickaxe.prefab
@@ -0,0 +1,124 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &7492862814627084760
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 3311562868863992538}
+ m_Layer: 0
+ m_Name: Pickaxe
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &3311562868863992538
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7492862814627084760}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -4.36068, y: 2, z: -36.9}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 3026504303214829544}
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8843044757232415404
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 3026504303214829544}
+ - component: {fileID: 5528544188432120844}
+ - component: {fileID: 8380855244601599694}
+ m_Layer: 0
+ m_Name: GameObject
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &3026504303214829544
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8843044757232415404}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 3311562868863992538}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &5528544188432120844
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8843044757232415404}
+ m_Mesh: {fileID: 1616382711435183797, guid: 804d477fc7f114c498aa6f95452be893, type: 3}
+--- !u!23 &8380855244601599694
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8843044757232415404}
+ 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: d64c307f1b4197c44970c29f9845c245, 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}
diff --git a/Assets/Resources/Prefabs/Pickaxe.prefab.meta b/Assets/Resources/Prefabs/Pickaxe.prefab.meta
new file mode 100644
index 0000000..498570b
--- /dev/null
+++ b/Assets/Resources/Prefabs/Pickaxe.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 4e920eca75dd2f44685f2be6a29e8b43
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scenes/GameMain.unity b/Assets/Scenes/GameMain.unity
index 5249b3a..6a0798e 100644
--- a/Assets/Scenes/GameMain.unity
+++ b/Assets/Scenes/GameMain.unity
@@ -119,6 +119,80 @@ NavMeshSettings:
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
+--- !u!1 &48181808
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 48181811}
+ - component: {fileID: 48181809}
+ - component: {fileID: 48181810}
+ m_Layer: 0
+ m_Name: PlayerSpawnPositionSetter
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &48181809
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 48181808}
+ 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: 2243665744
+ 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 &48181810
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 48181808}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 49cd9c4e7c611b04c8740c9e049129b9, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::Northbound.PlayerSpawnPositionSetter
+ ShowTopMostFoldoutHeaderGroup: 1
+ spawnPoints: []
+ useRandomSpawn: 0
+ findSpawnPointsAutomatically: 1
+--- !u!4 &48181811
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 48181808}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -0.03658, y: 1.00002, z: -4.65261}
+ 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 &61373298
GameObject:
m_ObjectHideFlags: 0
@@ -2102,7 +2176,7 @@ MonoBehaviour:
LoadSceneTimeOut: 120
SpawnTimeout: 10
EnableNetworkLogs: 1
- NetworkTopology: 0
+ NetworkTopology: 1
UseCMBService: 0
AutoSpawnPlayerPrefabClientSide: 1
NetworkMessageMetrics: 1
@@ -2440,12 +2514,12 @@ MonoBehaviour:
spawnWeight: 10
minCount: 0
maxCount: 228
- density: 0.02
+ density: 0.04
maxTotalObstacles: 6400
minDistanceBetweenObstacles: 4
alignToTerrain: 1
randomRotation: 1
- scaleVariation: 0.197
+ scaleVariation: 0.2
maxAttempts: 50
checkCollision: 1
collisionLayers:
@@ -3042,6 +3116,63 @@ Transform:
m_CorrespondingSourceObject: {fileID: 922888705413710451, guid: 5662d0b0d0eb5f54290edd8dd0980b57, type: 3}
m_PrefabInstance: {fileID: 2098115307}
m_PrefabAsset: {fileID: 0}
+--- !u!1001 &957391555319539118
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: 6825883197719469110, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: -5.09173
+ objectReference: {fileID: 0}
+ - target: {fileID: 6825883197719469110, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 2
+ objectReference: {fileID: 0}
+ - target: {fileID: 6825883197719469110, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: -37.867
+ objectReference: {fileID: 0}
+ - target: {fileID: 6825883197719469110, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 6825883197719469110, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 6825883197719469110, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 6825883197719469110, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 6825883197719469110, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 6825883197719469110, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 6825883197719469110, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 9113353226622644240, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
+ propertyPath: m_Name
+ value: Bat
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects: []
+ m_AddedComponents: []
+ m_SourcePrefab: {fileID: 100100000, guid: 66fa238e7512e1547bf2ef4b62d76afd, type: 3}
--- !u!1001 &1440648431994998967
PrefabInstance:
m_ObjectHideFlags: 0
@@ -3383,6 +3514,10 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
+ - target: {fileID: -7869551286978933574, guid: f395fcc064a3a834ba957327f1387c19, type: 3}
+ propertyPath: equipmentData.equipmentPrefab
+ value:
+ objectReference: {fileID: 7492862814627084760, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
- target: {fileID: 3247786716306397435, guid: f395fcc064a3a834ba957327f1387c19, type: 3}
propertyPath: m_LocalPosition.x
value: -30
@@ -3502,6 +3637,63 @@ Transform:
m_CorrespondingSourceObject: {fileID: 5749230937810543840, guid: 88f7f1e8a019b674498ab5fd494c1d34, type: 3}
m_PrefabInstance: {fileID: 6204924723497734287}
m_PrefabAsset: {fileID: 0}
+--- !u!1001 &6296397022839506560
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: 3311562868863992538, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: -4.36068
+ objectReference: {fileID: 0}
+ - target: {fileID: 3311562868863992538, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 2
+ objectReference: {fileID: 0}
+ - target: {fileID: 3311562868863992538, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: -36.9
+ objectReference: {fileID: 0}
+ - target: {fileID: 3311562868863992538, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 3311562868863992538, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3311562868863992538, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3311562868863992538, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3311562868863992538, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3311562868863992538, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3311562868863992538, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 7492862814627084760, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
+ propertyPath: m_Name
+ value: Pickaxe
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects: []
+ m_AddedComponents: []
+ m_SourcePrefab: {fileID: 100100000, guid: 4e920eca75dd2f44685f2be6a29e8b43, type: 3}
--- !u!114 &8940572951313384066 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 1287070985890992582, guid: e56926eda34629f4fbf3e4c53f0f8bd4, type: 3}
@@ -3603,3 +3795,6 @@ SceneRoots:
- {fileID: 1442785555}
- {fileID: 1440648431994998967}
- {fileID: 1061936651}
+ - {fileID: 48181811}
+ - {fileID: 957391555319539118}
+ - {fileID: 6296397022839506560}
diff --git a/Assets/Scripts/AttackAction.cs b/Assets/Scripts/AttackAction.cs
index 6c8f5fd..c524cd0 100644
--- a/Assets/Scripts/AttackAction.cs
+++ b/Assets/Scripts/AttackAction.cs
@@ -31,8 +31,14 @@ namespace Northbound
private Animator _animator;
private ITeamMember _teamMember;
private EquipmentSocket _equipmentSocket;
- private bool _isAttacking = false;
+ private NetworkVariable _isAttacking = new NetworkVariable(
+ false,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Owner
+ );
private bool _isWeaponEquipped = false;
+ private float _attackStartTime;
+ private const float ATTACK_TIMEOUT = 2f; // Auto-reset if animation event doesn't fire
private void Awake()
{
@@ -43,7 +49,7 @@ namespace Northbound
public bool CanExecute(ulong playerId)
{
- if (blockDuringAnimation && _isAttacking)
+ if (blockDuringAnimation && _isAttacking.Value)
return false;
return Time.time - _lastAttackTime >= attackCooldown;
@@ -55,7 +61,10 @@ namespace Northbound
return;
_lastAttackTime = Time.time;
- _isAttacking = true;
+ _attackStartTime = Time.time;
+
+ // Owner writes directly (Owner permission on _isAttacking)
+ _isAttacking.Value = true;
// 장비 장착 (애니메이션 이벤트 사용 안 할 경우)
if (!useAnimationEvents && useEquipment && !_isWeaponEquipped)
@@ -73,7 +82,7 @@ namespace Northbound
if (_animator == null || string.IsNullOrEmpty(attackAnimationTrigger))
{
PerformAttack();
- _isAttacking = false;
+ _isAttacking.Value = false;
}
}
@@ -168,7 +177,10 @@ namespace Northbound
public void OnAttackComplete()
{
- _isAttacking = false;
+ if (IsOwner)
+ {
+ _isAttacking.Value = false;
+ }
if (useEquipment && equipmentData != null && equipmentData.detachOnEnd && !equipmentData.keepEquipped)
{
@@ -183,16 +195,10 @@ namespace Northbound
private void AttachWeapon(string socketName = null)
{
if (_equipmentSocket == null || equipmentData == null)
- {
- Debug.LogWarning("[AttackAction] EquipmentSocket 또는 EquipmentData가 없습니다.");
return;
- }
if (equipmentData.equipmentPrefab == null)
- {
- Debug.LogWarning("[AttackAction] 무기 프리팹이 설정되지 않았습니다.");
return;
- }
string socket = socketName ?? equipmentData.socketName;
_equipmentSocket.AttachToSocket(socket, equipmentData.equipmentPrefab);
@@ -251,6 +257,34 @@ namespace Northbound
Gizmos.DrawWireSphere(attackOrigin, attackRange);
}
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ // Subscribe to attack state changes
+ _isAttacking.OnValueChanged += OnAttackStateChanged;
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ _isAttacking.OnValueChanged -= OnAttackStateChanged;
+ base.OnNetworkDespawn();
+ }
+
+ private void OnAttackStateChanged(bool previousValue, bool newValue)
+ {
+ }
+
+ public void Update()
+ {
+ if (!IsOwner) return;
+
+ if (_isAttacking.Value && Time.time - _attackStartTime > ATTACK_TIMEOUT)
+ {
+ _isAttacking.Value = false;
+ }
+ }
+
public override void OnDestroy()
{
// 무기 정리
@@ -262,6 +296,6 @@ namespace Northbound
base.OnDestroy();
}
- public bool IsAttacking => _isAttacking;
+ public bool IsAttacking => _isAttacking.Value;
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/AttackAction.cs.meta b/Assets/Scripts/AttackAction.cs.meta
index fcc99fe..8a9c8bd 100644
--- a/Assets/Scripts/AttackAction.cs.meta
+++ b/Assets/Scripts/AttackAction.cs.meta
@@ -1,2 +1,2 @@
fileFormatVersion: 2
-guid: 66ec3984614d8a64b8eae821376d038d
\ No newline at end of file
+guid: c467f66382052bc429affc5cb01d17db
\ No newline at end of file
diff --git a/Assets/Scripts/AutoHost.cs b/Assets/Scripts/AutoHost.cs
index 8f51ca7..17d616a 100644
--- a/Assets/Scripts/AutoHost.cs
+++ b/Assets/Scripts/AutoHost.cs
@@ -3,24 +3,63 @@ using Unity.Netcode;
public class AutoHost : MonoBehaviour
{
- // 에디터에서만 작동하도록 설정
+#if UNITY_EDITOR
+ private bool _hasStarted = false;
+
void Start()
{
-#if UNITY_EDITOR
- // 1. NetworkManager가 씬에 존재하는지 확인
- if (NetworkManager.Singleton != null)
- {
- // 2. 이미 서버나 클라이언트가 실행 중이 아닐 때만 실행
- if (!NetworkManager.Singleton.IsServer && !NetworkManager.Singleton.IsClient)
- {
- NetworkManager.Singleton.StartHost();
- Debug.Log("[AutoHost] 에디터 전용 호스트 자동 시작됨");
- }
- }
- else
+ if (NetworkManager.Singleton == null)
{
Debug.LogError("[AutoHost] NetworkManager를 찾을 수 없습니다!");
+ return;
}
-#endif
+
+ if (NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsClient)
+ {
+ return;
+ }
+
+ TryStartAsHost();
}
+
+ private void TryStartAsHost()
+ {
+ try
+ {
+ NetworkManager.Singleton.StartHost();
+ _hasStarted = true;
+ Debug.Log("[AutoHost] 호스트로 시작됨 (MAIN EDITOR)");
+ }
+ catch (System.Exception e)
+ {
+ Debug.Log($"[AutoHost] 호스트 시작 실패: {e.Message}");
+ Debug.Log("[AutoHost] 클라이언트 모드로 전환...");
+
+ NetworkManager.Singleton.Shutdown();
+ TryStartAsClient();
+ }
+ }
+
+ private void TryStartAsClient()
+ {
+ try
+ {
+ NetworkManager.Singleton.StartClient();
+ _hasStarted = true;
+ Debug.Log("[AutoHost] 클라이언트로 연결됨 (SECONDARY EDITOR)");
+ }
+ catch (System.Exception e)
+ {
+ Debug.LogError($"[AutoHost] 클라이언트 연결 실패: {e.Message}");
+ }
+ }
+
+ private void OnDestroy()
+ {
+ if (NetworkManager.Singleton != null)
+ {
+ NetworkManager.Singleton.Shutdown();
+ }
+ }
+#endif
}
\ No newline at end of file
diff --git a/Assets/Scripts/AutoTargetSystem.cs b/Assets/Scripts/AutoTargetSystem.cs
index dccb706..6c0e06d 100644
--- a/Assets/Scripts/AutoTargetSystem.cs
+++ b/Assets/Scripts/AutoTargetSystem.cs
@@ -44,7 +44,7 @@ namespace Northbound
private void Update()
{
- if (!IsServer) return;
+ if (!IsOwner) return;
if (_teamMember == null) return;
if (Time.time - _lastAttackTime >= attackInterval)
diff --git a/Assets/Scripts/Building.cs b/Assets/Scripts/Building.cs
index 279f776..7f32ca6 100644
--- a/Assets/Scripts/Building.cs
+++ b/Assets/Scripts/Building.cs
@@ -4,9 +4,9 @@ using UnityEngine;
namespace Northbound
{
- public class Building : NetworkBehaviour, IDamageable, IVisionProvider, ITeamMember
+ public class Building : DamageableNetworkBehaviour, IVisionProvider, ITeamMember
{
- [Header("References")]
+ [Header("Building Data")]
public BuildingData buildingData;
[Header("Runtime Info")]
@@ -14,52 +14,34 @@ namespace Northbound
public int rotation; // 0-3 (0=0°, 1=90°, 2=180°, 3=270°)
[Header("Team")]
- [Tooltip("건물의 팀 (플레이어/적대세력/몬스터/중립)")]
+ [Tooltip("Building team (Player/Enemy/Monster/Neutral)")]
public TeamType initialTeam = TeamType.Player;
[Header("Ownership (for pre-placed buildings)")]
- [Tooltip("씬에 미리 배치된 건물의 경우 여기서 소유자 설정 (0 = 중립, 1+ = 플레이어 ID)")]
+ [Tooltip("For pre-placed buildings, set owner here (0 = neutral, 1+ = player ID)")]
public ulong initialOwnerId = 0;
- [Tooltip("사전 배치 건물인가요? 체크하면 initialOwnerId를 사용합니다")]
+ [Tooltip("Is this a pre-placed building? If checked, uses initialOwnerId")]
public bool useInitialOwner = false;
- [Header("Health")]
- public bool showHealthBar = true;
+ [Header("Health Bar")]
public GameObject healthBarPrefab;
- [Header("Visual Effects")]
- public GameObject destroyEffectPrefab;
- public GameObject damageEffectPrefab;
- public Transform effectSpawnPoint;
-
[Header("Debug")]
public bool showGridBounds = true;
public Color gridBoundsColor = Color.cyan;
- // 현재 체력
- private NetworkVariable _currentHealth = new NetworkVariable(
- 0,
- NetworkVariableReadPermission.Everyone,
- NetworkVariableWritePermission.Server
- );
-
- // 건물 소유자 (사전 배치 건물 또는 동적 건물 모두 지원)
private NetworkVariable _ownerId = new NetworkVariable(
0,
NetworkVariableReadPermission.Everyone,
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;
@@ -70,63 +52,54 @@ namespace Northbound
{
base.OnNetworkSpawn();
- if (IsServer)
+ if (IsOwner)
{
- // 체력 초기화
- if (_currentHealth.Value == 0)
+ if (maxHealth == 0)
{
- _currentHealth.Value = buildingData != null ? buildingData.maxHealth : 100;
+ maxHealth = buildingData != null ? buildingData.maxHealth : 100;
+ _currentHealth.Value = maxHealth;
}
- // 팀 초기화
if (_team.Value == TeamType.Neutral)
{
_team.Value = initialTeam;
}
- // 소유자 초기화 (사전 배치 건물 체크)
if (useInitialOwner && _ownerId.Value == 0)
{
_ownerId.Value = initialOwnerId;
- // Debug.Log($"[Building] 사전 배치 건물 '{buildingData?.buildingName ?? gameObject.name}' 소유자: {initialOwnerId}, 팀: {_team.Value}");
}
else if (!useInitialOwner && _ownerId.Value == 0)
{
- // 동적 생성 건물은 NetworkObject의 Owner 사용
_ownerId.Value = OwnerClientId;
}
-
+
_lastRegenTime = Time.time;
- // FogOfWar 시스템에 시야 제공자로 등록
if (buildingData != null && buildingData.providesVision)
{
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
}
}
- // 이벤트 구독
- _currentHealth.OnValueChanged += OnHealthValueChanged;
_team.OnValueChanged += OnTeamValueChanged;
- // 체력바 생성
if (showHealthBar && healthBarPrefab != null)
{
- CreateHealthBar();
+ base.InitializeHealthBar();
}
- // 초기 체력 UI 업데이트
UpdateHealthUI();
UpdateTeamVisuals();
}
public override void OnNetworkDespawn()
{
- _currentHealth.OnValueChanged -= OnHealthValueChanged;
+ base.OnNetworkDespawn();
+
_team.OnValueChanged -= OnTeamValueChanged;
- // FogOfWar 시스템에서 제거
- if (IsServer && buildingData != null && buildingData.providesVision)
+ if (IsOwner && buildingData != null && buildingData.providesVision)
{
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
}
@@ -134,38 +107,33 @@ namespace Northbound
private void Update()
{
- if (!IsServer || buildingData == null)
+ if (!IsOwner || buildingData == null)
return;
- // 자동 체력 회복
- if (buildingData.autoRegenerate && _currentHealth.Value < buildingData.maxHealth)
+ if (buildingData.autoRegenerate && _currentHealth.Value < maxHealth)
{
if (Time.time - _lastRegenTime >= 1f)
{
- int regenAmount = Mathf.Min(buildingData.regenPerSecond, buildingData.maxHealth - _currentHealth.Value);
+ int regenAmount = Mathf.Min(buildingData.regenPerSecond, maxHealth - _currentHealth.Value);
_currentHealth.Value += regenAmount;
_lastRegenTime = Time.time;
}
}
}
- ///
- /// 건물 초기화 (BuildingManager가 동적 생성 시 호출)
- ///
public void Initialize(BuildingData data, Vector3Int gridPos, int rot, ulong ownerId, TeamType team = TeamType.Player)
{
buildingData = data;
gridPosition = gridPos;
rotation = rot;
- // 이미 스폰된 경우
- if (IsServer && IsSpawned)
+ if (IsOwner && IsSpawned)
{
- _currentHealth.Value = data.maxHealth;
+ maxHealth = data.maxHealth;
+ _currentHealth.Value = maxHealth;
_ownerId.Value = ownerId;
_team.Value = team;
-
- // 시야 제공자 등록
+
if (data.providesVision)
{
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
@@ -175,22 +143,28 @@ namespace Northbound
_isInitialized = true;
}
- ///
- /// 건물 소유권 변경 (점령 등)
- ///
public void SetOwner(ulong newOwnerId, TeamType newTeam)
{
- if (!IsServer) return;
+ if (!IsOwner)
+ {
+ SetOwnerServerRpc(newOwnerId, newTeam);
+ return;
+ }
+ SetOwnerServerRpc(newOwnerId, newTeam);
+ }
+
+ [ServerRpc]
+ private void SetOwnerServerRpc(ulong newOwnerId, TeamType newTeam)
+ {
ulong previousOwner = _ownerId.Value;
TeamType previousTeam = _team.Value;
-
+
_ownerId.Value = newOwnerId;
_team.Value = newTeam;
- Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"} 소유권 변경: {previousOwner} → {newOwnerId}, 팀: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(newTeam)}");
+ Debug.Log($"[Building] {buildingData?.buildingName ?? "Building"} ownership changed: {previousOwner} → {newOwnerId}, team: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(newTeam)}");
- // 시야 제공자 재등록 (소유자가 바뀌었으므로)
if (buildingData != null && buildingData.providesVision)
{
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
@@ -204,7 +178,18 @@ namespace Northbound
public void SetTeam(TeamType team)
{
- if (!IsServer) return;
+ if (!IsOwner)
+ {
+ SetTeamServerRpc(team);
+ return;
+ }
+
+ SetTeamServerRpc(team);
+ }
+
+ [ServerRpc]
+ private void SetTeamServerRpc(TeamType team)
+ {
_team.Value = team;
}
@@ -212,17 +197,12 @@ namespace Northbound
{
OnTeamChanged?.Invoke(newValue);
UpdateTeamVisuals();
- Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"} 팀 변경: {TeamManager.GetTeamName(previousValue)} → {TeamManager.GetTeamName(newValue)}");
+ Debug.Log($"[Building] {buildingData?.buildingName ?? "Building"} team changed: {TeamManager.GetTeamName(previousValue)} → {TeamManager.GetTeamName(newValue)}");
}
private void UpdateTeamVisuals()
{
- // 팀 색상으로 건물 외곽선이나 이펙트 변경 가능
- // 예: Renderer의 emission 색상 변경
Color teamColor = TeamManager.GetTeamColor(_team.Value);
-
- // 여기에 실제 비주얼 업데이트 로직 추가
- // 예: outline shader, emission, particle system 색상 등
}
#endregion
@@ -240,65 +220,85 @@ namespace Northbound
public bool IsActive()
{
- // 건물이 스폰되어 있고, 파괴되지 않았으며, 시야 제공 설정이 켜져있어야 함
- return IsSpawned && !IsDestroyed() && buildingData != null && buildingData.providesVision;
+ return IsSpawned && !IsDead() && buildingData != null && buildingData.providesVision;
}
#endregion
- #region IDamageable Implementation
+ #region IDamageable Overrides
- public void TakeDamage(int damage, ulong attackerId)
+ protected override void Die(ulong killerId)
{
- if (!IsServer)
+ base.Die(killerId);
+
+ if (!IsOwner) return;
+
+ Debug.Log($"[Building] {buildingData?.buildingName ?? "Building"} ({TeamManager.GetTeamName(_team.Value)}) destroyed! Attacker: {killerId}");
+
+ InvokeOnDestroyed();
+ NotifyDestroyedClientRpc();
+
+ if (buildingData != null && buildingData.providesVision)
{
- // 클라이언트는 서버에 요청
- TakeDamageServerRpc(damage, attackerId);
+ FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
+ }
+
+ if (BuildingManager.Instance != null)
+ {
+ BuildingManager.Instance.RemoveBuilding(this);
+ }
+
+ Invoke(nameof(DespawnBuilding), 0.5f);
+ }
+
+ private void DespawnBuilding()
+ {
+ if (IsOwner && NetworkObject != null)
+ {
+ NetworkObject.Despawn(true);
+ }
+ }
+
+ [ClientRpc]
+ private void NotifyDestroyedClientRpc()
+ {
+ if (!IsOwner)
+ {
+ InvokeOnDestroyed();
+ }
+ }
+
+ public override void TakeDamage(int damage, ulong attackerId)
+ {
+ if (!IsOwner)
+ {
+ TakeDamageOwnerRpc(damage, attackerId);
return;
}
- // 무적 건물
if (buildingData != null && buildingData.isIndestructible)
{
- Debug.Log($"[Building] {buildingData.buildingName}은(는) 무적입니다.");
+ Debug.Log($"[Building] {buildingData.buildingName} is indestructible.");
return;
}
- // 이미 파괴됨
- 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)} 팀을 공격할 수 없습니다.");
+ Debug.Log($"[Building] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} team cannot attack {TeamManager.GetTeamName(_team.Value)} team.");
return;
}
}
- // 데미지 적용
- int actualDamage = Mathf.Min(damage, _currentHealth.Value);
- _currentHealth.Value -= actualDamage;
-
- Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"} ({TeamManager.GetTeamName(_team.Value)})이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{buildingData?.maxHealth ?? 100}");
-
- // 데미지 이펙트
- ShowDamageEffectClientRpc();
-
- // 체력이 0이 되면 파괴
- if (_currentHealth.Value <= 0)
- {
- DestroyBuilding(attackerId);
- }
+ base.TakeDamage(damage, attackerId);
}
- [Rpc(SendTo.Server)]
- private void TakeDamageServerRpc(int damage, ulong attackerId)
+ [Rpc(SendTo.Owner)]
+ private void TakeDamageOwnerRpc(int damage, ulong attackerId)
{
TakeDamage(damage, attackerId);
}
@@ -307,179 +307,63 @@ namespace Northbound
#region Health Management
- ///
- /// 건물 파괴
- ///
- private void DestroyBuilding(ulong attackerId)
+ public new int GetMaxHealth()
{
- if (!IsServer)
- return;
-
- Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"} ({TeamManager.GetTeamName(_team.Value)})이(가) 파괴되었습니다! (공격자: {attackerId})");
-
- // 파괴 이벤트 발생
- OnDestroyed?.Invoke();
- NotifyDestroyedClientRpc();
-
- // 파괴 이펙트
- ShowDestroyEffectClientRpc();
-
- // FogOfWar 시스템에서 제거
- if (buildingData != null && buildingData.providesVision)
- {
- FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
- }
-
- // BuildingManager에서 제거
- if (BuildingManager.Instance != null)
- {
- BuildingManager.Instance.RemoveBuilding(this);
- }
-
- // 네트워크 오브젝트 파괴 (약간의 딜레이)
- Invoke(nameof(DespawnBuilding), 0.5f);
+ return buildingData != null ? buildingData.maxHealth : 100;
}
- private void DespawnBuilding()
+ public new float GetHealthPercentage()
{
- if (IsServer && NetworkObject != null)
- {
- NetworkObject.Despawn(true);
- }
+ int maxHp = GetMaxHealth();
+ return maxHp > 0 ? (float)_currentHealth.Value / maxHp : 0f;
}
- ///
- /// 체력 회복
- ///
- public void Heal(int amount)
- {
- if (!IsServer)
- return;
-
- if (buildingData == null)
- return;
-
- int healAmount = Mathf.Min(amount, buildingData.maxHealth - _currentHealth.Value);
- _currentHealth.Value += healAmount;
-
- Debug.Log($"[Building] {buildingData.buildingName}이(가) {healAmount} 회복되었습니다. 현재 체력: {_currentHealth.Value}/{buildingData.maxHealth}");
- }
-
- ///
- /// 현재 체력
- ///
- public int GetCurrentHealth() => _currentHealth.Value;
-
- ///
- /// 최대 체력
- ///
- public int GetMaxHealth() => buildingData != null ? buildingData.maxHealth : 100;
-
- ///
- /// 체력 비율 (0.0 ~ 1.0)
- ///
- public float GetHealthPercentage()
- {
- int maxHealth = GetMaxHealth();
- return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f;
- }
-
- ///
- /// 파괴되었는지 여부
- ///
public bool IsDestroyed() => _currentHealth.Value <= 0;
#endregion
#region Health UI
- private void CreateHealthBar()
+ protected override void InitializeHealthBar()
{
if (_healthBar != null)
return;
- GameObject healthBarObj = Instantiate(healthBarPrefab, transform);
- _healthBar = healthBarObj.GetComponent();
-
- if (_healthBar != null)
+ if (healthBarPrefab != null)
{
- _healthBar.Initialize(this);
+ GameObject healthBarObj = Instantiate(healthBarPrefab, transform);
+ _healthBar = healthBarObj.GetComponent();
+
+ if (_healthBar != null)
+ {
+ _healthBar.Initialize(this);
+ }
}
}
- private void UpdateHealthUI()
+ protected override void UpdateHealthUI()
{
if (_healthBar != null)
{
_healthBar.UpdateHealth(_currentHealth.Value, GetMaxHealth());
}
- OnHealthChanged?.Invoke(_currentHealth.Value, GetMaxHealth());
- }
-
- private void OnHealthValueChanged(int previousValue, int newValue)
- {
- UpdateHealthUI();
- }
-
- #endregion
-
- #region Visual Effects
-
- [ClientRpc]
- private void ShowDamageEffectClientRpc()
- {
- if (damageEffectPrefab != null)
- {
- Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform;
- GameObject effect = Instantiate(damageEffectPrefab, spawnPoint.position, spawnPoint.rotation);
- Destroy(effect, 2f);
- }
- }
-
- [ClientRpc]
- private void ShowDestroyEffectClientRpc()
- {
- if (destroyEffectPrefab != null)
- {
- Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform;
- GameObject effect = Instantiate(destroyEffectPrefab, spawnPoint.position, spawnPoint.rotation);
- Destroy(effect, 3f);
- }
- }
-
- [ClientRpc]
- private void NotifyDestroyedClientRpc()
- {
- if (!IsServer)
- {
- OnDestroyed?.Invoke();
- }
+ InvokeOnHealthChanged(_currentHealth.Value, GetMaxHealth());
}
#endregion
#region Grid Bounds
- ///
- /// Gets the grid-based bounds (from BuildingData width/length/height)
- /// This is used for placement validation, NOT the actual collider bounds
- /// Bounds are slightly shrunk to allow adjacent buildings to touch
- ///
public Bounds GetGridBounds()
{
if (buildingData == null) return new Bounds(transform.position, Vector3.one);
Vector3 gridSize = buildingData.GetSize(rotation);
-
- // Shrink slightly to allow buildings to be adjacent without Intersects() returning true
Vector3 shrunkSize = gridSize - Vector3.one * 0.01f;
return new Bounds(transform.position + Vector3.up * gridSize.y * 0.5f, shrunkSize);
}
- ///
- /// Legacy method, use GetGridBounds() instead
- ///
public Bounds GetBounds()
{
return GetGridBounds();
@@ -495,50 +379,15 @@ namespace Northbound
Bounds bounds = GetGridBounds();
- // 팀 색상으로 표시
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);
- }
- private void OnDrawGizmosSelected()
- {
- if (buildingData == null) return;
+ Gizmos.color = teamColor;
+ Gizmos.DrawWireSphere(transform.position + Vector3.up * 2f, 0.5f);
- Bounds bounds = GetGridBounds();
- Gizmos.color = Color.yellow;
- Gizmos.DrawWireCube(bounds.center, bounds.size);
-
- // Draw grid position
- if (BuildingManager.Instance != null)
- {
- Vector3 worldPos = BuildingManager.Instance.GridToWorld(gridPosition);
- Gizmos.color = Color.magenta;
- Gizmos.DrawSphere(worldPos, 0.2f);
- }
-
- // Draw vision range (if provides vision)
- if (buildingData.providesVision)
- {
- Gizmos.color = Color.cyan;
- Gizmos.DrawWireSphere(transform.position, buildingData.visionRange);
- }
-
- // 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}\nTeam: {teamName}");
- }
- else if (useInitialOwner)
- {
- string teamName = TeamManager.GetTeamName(initialTeam);
- UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
- $"Initial Owner: {initialOwnerId}\nTeam: {teamName}");
- }
- #endif
+ UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
+ $"{buildingData.buildingName}\nHP: {_currentHealth.Value}/{maxHealth}");
}
#endregion
diff --git a/Assets/Scripts/BuildingFoundation.cs b/Assets/Scripts/BuildingFoundation.cs
index 6b3ca48..d81088f 100644
--- a/Assets/Scripts/BuildingFoundation.cs
+++ b/Assets/Scripts/BuildingFoundation.cs
@@ -75,22 +75,32 @@ namespace Northbound
///
public void Initialize(BuildingData data, Vector3Int pos, int rot, ulong ownerId, TeamType team)
{
- if (!IsServer) return;
+ if (!IsOwner)
+ {
+ InitializeServerRpc(data != null ? data.buildingName : "", pos.x, pos.y, pos.z, rot, ownerId, team);
+ return;
+ }
- buildingData = data;
- gridPosition = pos;
+ InitializeServerRpc(data != null ? data.buildingName : "", pos.x, pos.y, pos.z, rot, ownerId, team);
+ }
+
+ [ServerRpc]
+ private void InitializeServerRpc(string buildingName, int posX, int posY, int posZ, int rot, ulong ownerId, TeamType team)
+ {
+ buildingData = BuildingManager.Instance?.availableBuildings.Find(b => b != null && b.buildingName == buildingName);
+ gridPosition = new Vector3Int(posX, posY, posZ);
rotation = rot;
_ownerId.Value = ownerId;
_team.Value = team;
_currentProgress.Value = 0f;
// BuildingData의 크기를 기반으로 스케일 설정
- Vector3 size = data.GetSize(rot);
-
+ Vector3 size = buildingData != null ? buildingData.GetSize(rot) : Vector3.one;
+
// foundationVisual의 스케일만 조정 (토대 자체의 pivot은 중앙에 유지)
if (foundationVisual != null)
{
- // 토대 비주얼을 건물 크기에 맞게 조정 (높이는 얇게)
+ // 토대 높이를 건물 크기에 맞게 조정 (높이는 얇게)
foundationVisual.transform.localScale = new Vector3(size.x, 0.2f, size.z);
foundationVisual.transform.localPosition = new Vector3(0, 0.1f, 0); // 바닥에서 약간 위
}
@@ -102,12 +112,31 @@ namespace Northbound
_collider = gameObject.AddComponent();
}
- // 상호작용 가능한 크기로 설정 (전체 건물 높이가 아닌 접근 가능한 크기)
+ // 상호작용 가능한 크기로 설정 (전체 건물 높이가 아니라 접근 가능한 크기)
_collider.size = new Vector3(size.x, 2f, size.z); // 높이를 2m로 설정하여 상호작용 가능
_collider.center = new Vector3(0, 1f, 0); // 중심을 1m 높이에 배치
_collider.isTrigger = false; // Trigger가 아닌 일반 Collider로 설정
- Debug.Log($"[BuildingFoundation] 토대 생성: {data.buildingName}, 크기: {size}, 위치: {transform.position}, Collider: {_collider.size}, 소유자: {ownerId}, 팀: {team}");
+ Debug.Log($"[BuildingFoundation] 토대 생성: {buildingData?.buildingName ?? "Building"}, 크기: {size}, 위치: {transform.position}, Collider: {_collider.size}, 소유자: {ownerId}, 팀: {TeamManager.GetTeamName(team)}");
+ }
+
+ private void UpdateProgressBar()
+ {
+ if (buildingData == null || _progressBarInstance == null)
+ return;
+
+ float progress = buildingData.requiredWorkAmount > 0 ? _currentProgress.Value / buildingData.requiredWorkAmount : 1f;
+
+ // 간단한 progress bar update - 필요한 경우 BuildingProgressBar 컴포넌트 사용
+ var progressBarTransform = _progressBarInstance.transform;
+ progressBarTransform.localScale = new Vector3(progress, 1f, 1f);
+ }
+
+ private void OnProgressValueChanged(float previousValue, float newValue)
+ {
+ UpdateProgressBar();
+ float max = buildingData != null ? buildingData.requiredWorkAmount : 100f;
+ OnProgressChanged?.Invoke(newValue, max);
}
///
@@ -123,6 +152,11 @@ namespace Northbound
return new Bounds(transform.position + Vector3.up * size.y * 0.5f, size);
}
+ public Bounds GetBounds()
+ {
+ return GetGridBounds();
+ }
+
#region IInteractable Implementation
public bool CanInteract(ulong playerId)
@@ -139,7 +173,7 @@ namespace Northbound
Debug.Log($"[BuildingFoundation] Already completed");
return false;
}
-
+
// 같은 팀만 건설 가능 - 플레이어의 팀을 가져와서 비교
TeamType playerTeam = GetPlayerTeam(playerId);
if (playerTeam != _team.Value)
@@ -153,7 +187,8 @@ namespace Northbound
public void Interact(ulong playerId)
{
- if (!IsServer || buildingData == null) return;
+ if (!IsServer || buildingData == null)
+ return;
if (!CanInteract(playerId))
return;
@@ -163,45 +198,71 @@ namespace Northbound
// 건설 진행
_currentProgress.Value += buildingData.workPerInteraction;
- Debug.Log($"[BuildingFoundation] 건설 진행: {_currentProgress.Value}/{buildingData.requiredWorkAmount} ({(_currentProgress.Value / buildingData.requiredWorkAmount * 100f):F1}%)");
+ Debug.Log($"[BuildingFoundation] {buildingData.buildingName} 건설 진행: {_currentProgress.Value}/{buildingData.requiredWorkAmount}");
- // 완성 체크
+ // 완료 체크
if (_currentProgress.Value >= buildingData.requiredWorkAmount)
{
- CompleteConstruction();
+ CompleteConstruction(playerId);
}
}
+ private void CompleteConstruction(ulong playerId)
+ {
+ Debug.Log($"[BuildingFoundation] {buildingData.buildingName} 건설 완료! 완성자: {playerId}");
+
+ OnConstructionComplete?.Invoke();
+
+ // 토대 디스폰
+ if (IsServer && NetworkObject != null)
+ {
+ NetworkObject.Despawn(true);
+ }
+
+ // 상호작용 UI 제거
+ if (_progressBarInstance != null)
+ {
+ Destroy(_progressBarInstance);
+ _progressBarInstance = null;
+ }
+ }
+
+ private TeamType GetPlayerTeam(ulong playerId)
+ {
+ if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(playerId, out NetworkObject playerObj))
+ {
+ var teamMember = playerObj.GetComponent();
+ if (teamMember != null)
+ {
+ return teamMember.GetTeam();
+ }
+ }
+ return TeamType.Neutral;
+ }
+
+ #endregion
+
+ #region IInteractable Implementation - Getters
+
public string GetInteractionPrompt()
{
if (buildingData == null)
- return "[E] 건설하기";
-
- float percentage = (_currentProgress.Value / buildingData.requiredWorkAmount) * 100f;
- return $"[E] {buildingData.buildingName} 건설 ({percentage:F0}%)";
+ return "건설하기";
+ float workNeeded = buildingData.requiredWorkAmount - _currentProgress.Value;
+ float interactionsNeeded = Mathf.Ceil(workNeeded / buildingData.workPerInteraction);
+ return $"[{interactionsNeeded}] 건설하기";
}
public string GetInteractionAnimation()
{
- // BuildingData에서 애니메이션 트리거 가져오기
- if (buildingData != null && !string.IsNullOrEmpty(buildingData.constructionAnimationTrigger))
- {
+ if (buildingData != null && buildingData.constructionAnimationTrigger != null)
return buildingData.constructionAnimationTrigger;
- }
-
- // 기본값: 빈 문자열 (애니메이션 없음)
- return "";
+ return null;
}
public EquipmentData GetEquipmentData()
{
- // BuildingData에 건설 도구가 정의되어 있으면 반환
- if (buildingData != null && buildingData.constructionEquipment != null)
- {
- return buildingData.constructionEquipment;
- }
-
- return null; // 특별한 도구 불필요
+ return buildingData != null ? buildingData.constructionEquipment : null;
}
public Transform GetTransform()
@@ -213,136 +274,25 @@ namespace Northbound
#region ITeamMember Implementation
- public TeamType GetTeam()
- {
- return _team.Value;
- }
+ public TeamType GetTeam() => _team.Value;
public void SetTeam(TeamType team)
{
- if (!IsServer) return;
+ if (!IsOwner)
+ {
+ SetTeamServerRpc(team);
+ return;
+ }
+
+ SetTeamServerRpc(team);
+ }
+
+ [ServerRpc]
+ private void SetTeamServerRpc(TeamType team)
+ {
_team.Value = team;
}
#endregion
-
- ///
- /// 플레이어의 팀 가져오기
- ///
- private TeamType GetPlayerTeam(ulong playerId)
- {
- // 플레이어의 NetworkObject 찾기
- if (NetworkManager.Singleton != null && NetworkManager.Singleton.SpawnManager != null)
- {
- if (NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client))
- {
- if (client.PlayerObject != null)
- {
- var teamMember = client.PlayerObject.GetComponent();
- if (teamMember != null)
- {
- return teamMember.GetTeam();
- }
- }
- }
- }
-
- // 기본값: 플레이어 팀
- return TeamType.Player;
- }
-
- private void CompleteConstruction()
- {
- if (!IsServer) return;
-
- Debug.Log($"[BuildingFoundation] 건물 완성! {buildingData.buildingName}");
-
- OnConstructionComplete?.Invoke();
-
- // BuildingManager에서 토대 제거
- var buildingManager = BuildingManager.Instance;
- if (buildingManager != null)
- {
- buildingManager.RemoveFoundation(this);
- }
-
- // 완성된 건물 생성
- SpawnCompletedBuilding();
-
- // 토대 제거
- if (NetworkObject != null)
- {
- NetworkObject.Despawn(true);
- }
- }
-
- private void SpawnCompletedBuilding()
- {
- if (!IsServer || buildingData == null || buildingData.prefab == null)
- return;
-
- // BuildingManager를 통해 건물 생성
- var buildingManager = BuildingManager.Instance;
- if (buildingManager != null)
- {
- buildingManager.SpawnCompletedBuildingServerRpc(
- buildingData.name,
- gridPosition,
- rotation,
- _ownerId.Value,
- _team.Value
- );
- }
- else
- {
- Debug.LogError("[BuildingFoundation] BuildingManager를 찾을 수 없습니다!");
- }
- }
-
- private void OnProgressValueChanged(float oldValue, float newValue)
- {
- if (buildingData != null)
- {
- OnProgressChanged?.Invoke(newValue, buildingData.requiredWorkAmount);
- }
- UpdateProgressBar();
- }
-
- private void UpdateProgressBar()
- {
- if (_progressBarInstance == null || buildingData == null) return;
-
- // 진행바 UI 업데이트 (BuildingHealthBar와 유사한 구조 사용 가능)
- var progressBar = _progressBarInstance.GetComponent();
- if (progressBar != null)
- {
- // BuildingHealthBar를 재사용하여 진행도 표시
- progressBar.UpdateHealth((int)_currentProgress.Value, (int)buildingData.requiredWorkAmount);
- }
- }
-
- private void OnDrawGizmos()
- {
- if (buildingData == null) return;
-
- // 건물 경계 표시 (노란색)
- Gizmos.color = Color.yellow;
- Vector3 size = buildingData.GetSize(rotation);
- Gizmos.DrawWireCube(transform.position + Vector3.up * size.y * 0.5f, size);
-
- // Collider 경계 표시 (초록색)
- if (_collider != null)
- {
- Gizmos.color = Color.green;
- Gizmos.DrawWireCube(transform.position + _collider.center, _collider.size);
- }
-
- // 상호작용 가능 여부 표시
- if (_currentProgress.Value < (buildingData?.requiredWorkAmount ?? 100f))
- {
- Gizmos.color = Color.cyan;
- Gizmos.DrawSphere(transform.position + Vector3.up, 0.3f);
- }
- }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Scripts/BuildingManager.cs b/Assets/Scripts/BuildingManager.cs
index 27d0072..b904d4f 100644
--- a/Assets/Scripts/BuildingManager.cs
+++ b/Assets/Scripts/BuildingManager.cs
@@ -16,7 +16,7 @@ namespace Northbound
public List availableBuildings = new List();
[Header("Foundation Settings")]
- public GameObject foundationPrefab; // 토대 프리팹 (Inspector에서 할당)
+ public GameObject foundationPrefab;
private List placedBuildings = new List();
private List placedFoundations = new List();
@@ -58,6 +58,53 @@ namespace Northbound
);
}
+ private bool ValidateClient(ulong clientId)
+ {
+ if (!NetworkManager.Singleton.ConnectedClients.ContainsKey(clientId))
+ {
+ Debug.LogWarning($"[BuildingManager] 유효하지 않은 클라이언트 ID: {clientId}");
+ return false;
+ }
+ return true;
+ }
+
+ private bool ValidateBuildingIndex(int buildingIndex)
+ {
+ if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count)
+ {
+ Debug.LogWarning($"[BuildingManager] 유효하지 않은 건물 인덱스: {buildingIndex}");
+ return false;
+ }
+ return true;
+ }
+
+ private BuildingData GetBuildingData(int buildingIndex)
+ {
+ BuildingData data = availableBuildings[buildingIndex];
+ if (data == null || data.prefab == null)
+ {
+ Debug.LogWarning($"[BuildingManager] 건물 데이터가 유효하지 않습니다.");
+ return null;
+ }
+ return data;
+ }
+
+ private void SetupSpawnedObject(GameObject obj, ulong ownerId)
+ {
+ if (obj.GetComponent() == null)
+ {
+ var visibility = obj.AddComponent();
+ visibility.showInExploredAreas = true;
+ visibility.updateInterval = 0.2f;
+ }
+
+ NetworkObject netObj = obj.GetComponent();
+ if (netObj != null)
+ {
+ netObj.SpawnWithOwnership(ownerId);
+ }
+ }
+
public bool IsValidPlacement(BuildingData data, Vector3 position, int rotation, out Vector3 groundPosition)
{
groundPosition = position;
@@ -141,8 +188,10 @@ namespace Northbound
{
groundPosition = position;
- // Raycast down to find ground
- if (Physics.Raycast(position + Vector3.up * 10f, Vector3.down, out RaycastHit hit, 20f, groundLayer))
+ float originHeight = 10f;
+ float rayDistance = 20f;
+ Vector3 rayOrigin = position + Vector3.up * originHeight;
+ if (Physics.Raycast(rayOrigin, Vector3.down, out RaycastHit hit, rayDistance, groundLayer))
{
groundPosition = hit.point;
return true;
@@ -169,30 +218,22 @@ namespace Northbound
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void PlaceBuildingServerRpc(int buildingIndex, Vector3 position, int rotation, ulong requestingClientId)
{
- // 보안 검증 1: 유효한 클라이언트인지 확인
- if (!NetworkManager.Singleton.ConnectedClients.ContainsKey(requestingClientId))
+ if (!ValidateClient(requestingClientId))
{
- Debug.LogWarning($"[BuildingManager] 유효하지 않은 클라이언트 ID: {requestingClientId}");
return;
}
- // 보안 검증 2: 건물 인덱스 유효성 확인
- if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count)
+ if (!ValidateBuildingIndex(buildingIndex))
{
- Debug.LogWarning($"[BuildingManager] 유효하지 않은 건물 인덱스: {buildingIndex} (클라이언트: {requestingClientId})");
return;
}
- BuildingData data = availableBuildings[buildingIndex];
-
- // 보안 검증 3: 건물 데이터 유효성 확인
- if (data == null || data.prefab == null)
+ BuildingData data = GetBuildingData(buildingIndex);
+ if (data == null)
{
- Debug.LogWarning($"[BuildingManager] 건물 데이터가 유효하지 않습니다. (클라이언트: {requestingClientId})");
return;
}
- // 배치 가능 여부 확인
if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition))
{
Debug.LogWarning($"[BuildingManager] 건물 배치 불가능 위치 (클라이언트: {requestingClientId})");
@@ -200,41 +241,23 @@ namespace Northbound
}
Vector3Int gridPosition = WorldToGrid(snappedPosition);
-
- // 건물 생성
GameObject buildingObj = Instantiate(data.prefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
- NetworkObject netObj = buildingObj.GetComponent();
- // Add FogOfWarVisibility component to hide buildings in unexplored areas
- if (buildingObj.GetComponent() == null)
+ SetupSpawnedObject(buildingObj, requestingClientId);
+
+ Building building = buildingObj.GetComponent();
+ if (building == null)
{
- var visibility = buildingObj.AddComponent();
- visibility.showInExploredAreas = true; // Buildings remain visible in explored areas
- visibility.updateInterval = 0.2f;
- }
-
- if (netObj != null)
- {
- // 건물의 소유자를 설정
- netObj.SpawnWithOwnership(requestingClientId);
-
- Building building = buildingObj.GetComponent();
- if (building == null)
- {
- building = buildingObj.AddComponent();
- }
-
- // 건물 초기화
- building.Initialize(data, gridPosition, rotation, requestingClientId);
- placedBuildings.Add(building);
-
- Debug.Log($"[BuildingManager] {data.buildingName} 건설 완료 (소유자: {requestingClientId}, 위치: {gridPosition})");
- }
- else
- {
- Debug.LogError($"[BuildingManager] NetworkObject 컴포넌트가 없습니다! (Prefab: {data.prefab.name})");
+ Debug.LogError($"[BuildingManager] Building prefab must have Building component! (Prefab: {data.prefab.name})");
+ buildingObj.GetComponent()?.Despawn(true);
Destroy(buildingObj);
+ return;
}
+
+ building.Initialize(data, gridPosition, rotation, requestingClientId);
+ placedBuildings.Add(building);
+
+ Debug.Log($"[BuildingManager] {data.buildingName} 건설 완료 (소유자: {requestingClientId}, 위치: {gridPosition})");
}
public void RemoveBuilding(Building building)
@@ -321,37 +344,28 @@ namespace Northbound
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void PlaceFoundationServerRpc(int buildingIndex, Vector3 position, int rotation, ulong requestingClientId)
{
- // 보안 검증 1: 유효한 클라이언트인지 확인
- if (!NetworkManager.Singleton.ConnectedClients.ContainsKey(requestingClientId))
+ if (!ValidateClient(requestingClientId))
{
- Debug.LogWarning($"[BuildingManager] 유효하지 않은 클라이언트 ID: {requestingClientId}");
return;
}
- // 보안 검증 2: 건물 인덱스 유효성 확인
- if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count)
+ if (!ValidateBuildingIndex(buildingIndex))
{
- Debug.LogWarning($"[BuildingManager] 유효하지 않은 건물 인덱스: {buildingIndex}");
return;
}
- BuildingData data = availableBuildings[buildingIndex];
-
- // 보안 검증 3: 건물 데이터 유효성 확인
+ BuildingData data = GetBuildingData(buildingIndex);
if (data == null)
{
- Debug.LogWarning($"[BuildingManager] 건물 데이터가 유효하지 않습니다.");
return;
}
- // 토대 프리팹 확인
if (foundationPrefab == null)
{
Debug.LogError("[BuildingManager] foundationPrefab이 설정되지 않았습니다!");
return;
}
- // 배치 가능 여부 확인
if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition))
{
Debug.LogWarning($"[BuildingManager] 토대 배치 불가능 위치");
@@ -359,43 +373,23 @@ namespace Northbound
}
Vector3Int gridPosition = WorldToGrid(snappedPosition);
-
- // 플레이어 팀 가져오기
TeamType playerTeam = GetPlayerTeam(requestingClientId);
- // 토대 생성
GameObject foundationObj = Instantiate(foundationPrefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
- NetworkObject netObj = foundationObj.GetComponent();
- // Add FogOfWarVisibility component to hide foundations in unexplored areas
- if (foundationObj.GetComponent() == null)
+ SetupSpawnedObject(foundationObj, requestingClientId);
+
+ BuildingFoundation foundation = foundationObj.GetComponent();
+ if (foundation != null)
{
- var visibility = foundationObj.AddComponent();
- visibility.showInExploredAreas = true; // Foundations remain visible in explored areas
- visibility.updateInterval = 0.2f;
- }
-
- if (netObj != null)
- {
- netObj.SpawnWithOwnership(requestingClientId);
-
- BuildingFoundation foundation = foundationObj.GetComponent();
- if (foundation != null)
- {
- foundation.Initialize(data, gridPosition, rotation, requestingClientId, playerTeam);
- placedFoundations.Add(foundation); // 토대 목록에 추가
- Debug.Log($"[BuildingManager] {data.buildingName} 토대 생성 (소유자: {requestingClientId}, 위치: {gridPosition})");
- }
- else
- {
- Debug.LogError("[BuildingManager] BuildingFoundation 컴포넌트가 없습니다!");
- netObj.Despawn(true);
- }
+ foundation.Initialize(data, gridPosition, rotation, requestingClientId, playerTeam);
+ placedFoundations.Add(foundation);
+ Debug.Log($"[BuildingManager] {data.buildingName} 토대 생성 (소유자: {requestingClientId}, 위치: {gridPosition})");
}
else
{
- Debug.LogError("[BuildingManager] NetworkObject 컴포넌트가 없습니다!");
- Destroy(foundationObj);
+ Debug.LogError("[BuildingManager] BuildingFoundation 컴포넌트가 없습니다!");
+ foundationObj.GetComponent()?.Despawn(true);
}
}
@@ -427,7 +421,6 @@ namespace Northbound
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void SpawnCompletedBuildingServerRpc(string buildingDataName, Vector3Int gridPosition, int rotation, ulong ownerId, TeamType team)
{
- // BuildingData 찾기
BuildingData data = availableBuildings.Find(b => b.name == buildingDataName);
if (data == null || data.prefab == null)
{
@@ -436,40 +429,23 @@ namespace Northbound
}
Vector3 worldPosition = GridToWorld(gridPosition);
-
- // 완성된 건물 생성
GameObject buildingObj = Instantiate(data.prefab, worldPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
- NetworkObject netObj = buildingObj.GetComponent();
- // Add FogOfWarVisibility component to hide buildings in unexplored areas
- if (buildingObj.GetComponent() == null)
+ SetupSpawnedObject(buildingObj, ownerId);
+
+ Building building = buildingObj.GetComponent();
+ if (building == null)
{
- var visibility = buildingObj.AddComponent();
- visibility.showInExploredAreas = true; // Buildings remain visible in explored areas
- visibility.updateInterval = 0.2f;
- }
-
- if (netObj != null)
- {
- netObj.SpawnWithOwnership(ownerId);
-
- Building building = buildingObj.GetComponent();
- if (building == null)
- {
- building = buildingObj.AddComponent();
- }
-
- // 건물 초기화
- building.Initialize(data, gridPosition, rotation, ownerId);
- placedBuildings.Add(building);
-
- Debug.Log($"[BuildingManager] {data.buildingName} 건설 완료! (소유자: {ownerId}, 위치: {gridPosition}, 팀: {team})");
- }
- else
- {
- Debug.LogError($"[BuildingManager] NetworkObject 컴포넌트가 없습니다!");
+ Debug.LogError($"[BuildingManager] Building prefab must have Building component! (Prefab: {data.prefab.name})");
+ buildingObj.GetComponent()?.Despawn(true);
Destroy(buildingObj);
+ return;
}
+
+ building.Initialize(data, gridPosition, rotation, ownerId);
+ placedBuildings.Add(building);
+
+ Debug.Log($"[BuildingManager] {data.buildingName} 건설 완료! (소유자: {ownerId}, 위치: {gridPosition}, 팀: {team})");
}
}
}
diff --git a/Assets/Scripts/BuildingPlacement.cs b/Assets/Scripts/BuildingPlacement.cs
index 8022b63..2bdc179 100644
--- a/Assets/Scripts/BuildingPlacement.cs
+++ b/Assets/Scripts/BuildingPlacement.cs
@@ -225,61 +225,71 @@ namespace Northbound
private void CreatePreview()
{
+ if (!ValidateBuildingData(selectedBuildingIndex, out BuildingData data))
+ {
+ return;
+ }
+
+ previewObject = Instantiate(data.prefab);
+ SetupPreviewObject(previewObject, validMaterial);
+ Debug.Log($"[BuildingPlacement] 프리뷰 생성됨: {data.buildingName}");
+ }
+
+ private bool ValidateBuildingData(int index, out BuildingData data)
+ {
+ data = null;
+
if (BuildingManager.Instance == null)
{
Debug.LogWarning("[BuildingPlacement] BuildingManager가 없습니다.");
- return;
+ return false;
}
- if (selectedBuildingIndex < 0 || selectedBuildingIndex >= BuildingManager.Instance.availableBuildings.Count)
+ if (index < 0 || index >= BuildingManager.Instance.availableBuildings.Count)
{
- Debug.LogWarning($"[BuildingPlacement] 유효하지 않은 건물 인덱스: {selectedBuildingIndex}");
- return;
+ Debug.LogWarning($"[BuildingPlacement] 유효하지 않은 건물 인덱스: {index}");
+ return false;
}
- BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
+ data = BuildingManager.Instance.availableBuildings[index];
if (data == null || data.prefab == null)
{
Debug.LogWarning("[BuildingPlacement] BuildingData 또는 Prefab이 없습니다.");
- return;
+ return false;
}
- // 완성 건물 프리팹으로 프리뷰 생성 (사용자가 완성 모습을 볼 수 있도록)
- previewObject = Instantiate(data.prefab);
-
- // Remove NetworkObject component from preview
- NetworkObject netObj = previewObject.GetComponent();
+ return true;
+ }
+
+ private void SetupPreviewObject(GameObject previewObj, Material material)
+ {
+ NetworkObject netObj = previewObj.GetComponent();
if (netObj != null)
{
Destroy(netObj);
}
- // Remove Building component from preview
- Building building = previewObject.GetComponent();
+ Building building = previewObj.GetComponent();
if (building != null)
{
Destroy(building);
}
- // Apply ghost materials
- previewRenderers = previewObject.GetComponentsInChildren();
- foreach (var renderer in previewRenderers)
+ Renderer[] renderers = previewObj.GetComponentsInChildren();
+ foreach (var renderer in renderers)
{
Material[] mats = new Material[renderer.materials.Length];
for (int i = 0; i < mats.Length; i++)
{
- mats[i] = validMaterial;
+ mats[i] = material;
}
renderer.materials = mats;
}
- // Disable colliders in preview
- foreach (var collider in previewObject.GetComponentsInChildren())
+ foreach (var collider in previewObj.GetComponentsInChildren())
{
collider.enabled = false;
}
-
- Debug.Log($"[BuildingPlacement] 프리뷰 생성됨: {data.buildingName}");
}
private void DestroyPreview()
@@ -297,29 +307,33 @@ namespace Northbound
return;
Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue());
-
+
if (Physics.Raycast(ray, out RaycastHit hit, maxPlacementDistance, groundLayer))
{
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
- // Check if placement is valid
bool isValid = BuildingManager.Instance.IsValidPlacement(data, hit.point, currentRotation, out Vector3 snappedPosition);
- // Update preview position (placementOffset 적용)
previewObject.transform.position = snappedPosition + data.placementOffset;
previewObject.transform.rotation = Quaternion.Euler(0, currentRotation * 90f, 0);
- // Update material based on validity
Material targetMat = isValid ? validMaterial : invalidMaterial;
- foreach (var renderer in previewRenderers)
+ UpdatePreviewMaterials(previewRenderers, targetMat);
+ }
+ }
+
+ private void UpdatePreviewMaterials(Renderer[] renderers, Material material)
+ {
+ if (renderers == null) return;
+
+ foreach (var renderer in renderers)
+ {
+ Material[] mats = new Material[renderer.materials.Length];
+ for (int i = 0; i < mats.Length; i++)
{
- Material[] mats = new Material[renderer.materials.Length];
- for (int i = 0; i < mats.Length; i++)
- {
- mats[i] = targetMat;
- }
- renderer.materials = mats;
+ mats[i] = material;
}
+ renderer.materials = mats;
}
}
@@ -419,16 +433,13 @@ namespace Northbound
}
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
-
- // 드래그 영역 계산
+
Vector3 dragEndPosition = hit.point;
List positions = CalculateDragBuildingPositions(dragStartPosition, dragEndPosition, data);
- // 기존 프리뷰 정리
ClearDragPreviews();
-
- // 새로운 프리뷰 생성
dragBuildingPositions.Clear();
+
foreach (var pos in positions)
{
if (dragPreviewObjects.Count >= maxDragBuildingCount)
@@ -440,38 +451,14 @@ namespace Northbound
bool isValid = BuildingManager.Instance.IsValidPlacement(data, pos, currentRotation, out Vector3 snappedPosition);
GameObject preview = Instantiate(data.prefab);
-
- // Remove NetworkObject and Building components
- if (preview.GetComponent() != null)
- Destroy(preview.GetComponent());
- if (preview.GetComponent() != null)
- Destroy(preview.GetComponent());
+ Material targetMat = isValid ? validMaterial : invalidMaterial;
+ SetupPreviewObject(preview, targetMat);
- // Set position and rotation
preview.transform.position = snappedPosition + data.placementOffset;
preview.transform.rotation = Quaternion.Euler(0, currentRotation * 90f, 0);
- // Apply materials
- Material targetMat = isValid ? validMaterial : invalidMaterial;
- Renderer[] renderers = preview.GetComponentsInChildren();
- foreach (var renderer in renderers)
- {
- Material[] mats = new Material[renderer.materials.Length];
- for (int i = 0; i < mats.Length; i++)
- {
- mats[i] = targetMat;
- }
- renderer.materials = mats;
- }
-
- // Disable colliders
- foreach (var collider in preview.GetComponentsInChildren())
- {
- collider.enabled = false;
- }
-
dragPreviewObjects.Add(preview);
-
+
if (isValid)
{
dragBuildingPositions.Add(snappedPosition);
diff --git a/Assets/Scripts/Core.cs b/Assets/Scripts/Core.cs
index fe0cfd1..d7a0efa 100644
--- a/Assets/Scripts/Core.cs
+++ b/Assets/Scripts/Core.cs
@@ -50,7 +50,7 @@ namespace Northbound
public override void OnNetworkSpawn()
{
- if (IsServer)
+ if (IsOwner)
{
_totalResources.Value = 0;
_currentHealth.Value = maxHealth;
@@ -88,7 +88,7 @@ namespace Northbound
public void TakeDamage(int damage, ulong attackerId)
{
- if (!IsServer) return;
+ if (!IsOwner) return;
if (_currentHealth.Value <= 0) return;
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
@@ -108,7 +108,7 @@ namespace Northbound
private void OnCoreDestroyed()
{
- if (!IsServer) return;
+ if (!IsOwner) return;
Debug.Log($"[Core] 코어가 파괴되었습니다! 게임 오버!");
@@ -174,7 +174,7 @@ namespace Northbound
///
public void AddResource(int amount)
{
- if (!IsServer) return;
+ if (!IsOwner) return;
if (!unlimitedStorage)
{
@@ -206,7 +206,7 @@ namespace Northbound
{
if (client.PlayerObject != null)
{
- var playerInventory = client.PlayerObject.GetComponent();
+ var playerInventory = client.PlayerObject.GetComponent();
if (playerInventory != null)
{
// 플레이어가 자원을 가지고 있어야 함
@@ -264,10 +264,10 @@ namespace Northbound
if (playerObject == null)
return;
- var playerInventory = playerObject.GetComponent();
+ var playerInventory = playerObject.GetComponent();
if (playerInventory == null)
{
- Debug.LogWarning($"플레이어 {playerId}에게 PlayerResourceInventory 컴포넌트가 없습니다.");
+ Debug.LogWarning($"플레이어 {playerId}에게 PlayerInventory 컴포넌트가 없습니다.");
return;
}
@@ -304,8 +304,8 @@ namespace Northbound
return;
}
- // 플레이어로부터 자원 차감
- playerInventory.RemoveResourceServerRpc(depositAmount);
+ // 플레이어로부터 자원 차감 (RPC로 owner에게 요청)
+ playerInventory.RemoveResourcesRpc(depositAmount);
// 코어에 자원 추가
_totalResources.Value += depositAmount;
diff --git a/Assets/Scripts/DamageableNetworkBehaviour.cs b/Assets/Scripts/DamageableNetworkBehaviour.cs
new file mode 100644
index 0000000..999cb20
--- /dev/null
+++ b/Assets/Scripts/DamageableNetworkBehaviour.cs
@@ -0,0 +1,162 @@
+using System;
+using Unity.Netcode;
+using UnityEngine;
+
+namespace Northbound
+{
+ public abstract class DamageableNetworkBehaviour : NetworkBehaviour, IDamageable
+ {
+ [Header("Health Settings")]
+ [SerializeField] protected int maxHealth = 100;
+ [SerializeField] protected bool showHealthBar = true;
+
+ [Header("Visual Effects")]
+ [SerializeField] protected GameObject damageEffectPrefab;
+ [SerializeField] protected GameObject destroyEffectPrefab;
+ [SerializeField] protected Transform effectSpawnPoint;
+
+ protected NetworkVariable _currentHealth = new NetworkVariable(
+ 0,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ public event Action OnHealthChanged;
+ public event Action OnDestroyed;
+
+ protected void InvokeOnHealthChanged(int currentHealth, int maxHealth)
+ {
+ OnHealthChanged?.Invoke(currentHealth, maxHealth);
+ }
+
+ protected void InvokeOnDestroyed()
+ {
+ OnDestroyed?.Invoke();
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ if (IsOwner)
+ {
+ InitializeHealthServerRpc(maxHealth);
+ }
+
+ _currentHealth.OnValueChanged += OnHealthValueChanged;
+ InitializeHealthBar();
+ UpdateHealthUI();
+ }
+
+ [ServerRpc]
+ private void InitializeHealthServerRpc(int health)
+ {
+ if (_currentHealth.Value == 0)
+ _currentHealth.Value = health;
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ _currentHealth.OnValueChanged -= OnHealthValueChanged;
+ base.OnNetworkDespawn();
+ }
+
+ protected virtual void InitializeHealthBar()
+ {
+ }
+
+ protected virtual void UpdateHealthUI()
+ {
+ }
+
+ public virtual void TakeDamage(int damage, ulong attackerId)
+ {
+ if (!IsOwner)
+ {
+ TakeDamageServerRpc(damage, attackerId);
+ return;
+ }
+
+ TakeDamageServerRpc(damage, attackerId);
+ }
+
+ [ServerRpc]
+ private void TakeDamageServerRpc(int damage, ulong attackerId)
+ {
+ if (_currentHealth.Value <= 0)
+ return;
+
+ int actualDamage = Mathf.Min(damage, _currentHealth.Value);
+ _currentHealth.Value -= actualDamage;
+
+ Debug.Log($"[{GetType().Name}] {gameObject.name} received {actualDamage} damage. Health: {_currentHealth.Value}/{maxHealth}");
+
+ ShowDamageEffectClientRpc();
+
+ if (_currentHealth.Value <= 0)
+ {
+ Die(attackerId);
+ }
+ }
+
+ protected virtual void Die(ulong killerId)
+ {
+ Debug.Log($"[{GetType().Name}] {gameObject.name} destroyed! Killer: {killerId}");
+ OnDestroyed?.Invoke();
+ ShowDeathEffectClientRpc();
+ }
+
+ [ClientRpc]
+ protected void ShowDamageEffectClientRpc()
+ {
+ if (damageEffectPrefab != null)
+ {
+ Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform;
+ GameObject effect = Instantiate(damageEffectPrefab, spawnPoint.position + Vector3.up, Quaternion.identity);
+ Destroy(effect, 2f);
+ }
+ }
+
+ [ClientRpc]
+ protected void ShowDeathEffectClientRpc()
+ {
+ if (destroyEffectPrefab != null)
+ {
+ Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform;
+ GameObject effect = Instantiate(destroyEffectPrefab, spawnPoint.position, Quaternion.identity);
+ Destroy(effect, 3f);
+ }
+ }
+
+ protected virtual void OnHealthValueChanged(int previousValue, int newValue)
+ {
+ OnHealthChanged?.Invoke(newValue, maxHealth);
+ UpdateHealthUI();
+ }
+
+ public int GetCurrentHealth() => _currentHealth.Value;
+ public int GetMaxHealth() => maxHealth;
+ public float GetHealthPercentage() => maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f;
+ public bool IsDead() => _currentHealth.Value <= 0;
+
+ public virtual void Heal(int amount)
+ {
+ if (!IsOwner)
+ {
+ HealServerRpc(amount);
+ return;
+ }
+
+ HealServerRpc(amount);
+ }
+
+ [ServerRpc]
+ private void HealServerRpc(int amount)
+ {
+ int healAmount = Mathf.Min(amount, maxHealth - _currentHealth.Value);
+ _currentHealth.Value += healAmount;
+
+ Debug.Log($"[{GetType().Name}] {gameObject.name} healed {healAmount}. Health: {_currentHealth.Value}/{maxHealth}");
+ }
+ }
+}
diff --git a/Assets/Scripts/DamageableNetworkBehaviour.cs.meta b/Assets/Scripts/DamageableNetworkBehaviour.cs.meta
new file mode 100644
index 0000000..5999993
--- /dev/null
+++ b/Assets/Scripts/DamageableNetworkBehaviour.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b7aeb2eee85ba3c42ab4d53ef28f96fc
\ No newline at end of file
diff --git a/Assets/Scripts/EnemyAIController.cs b/Assets/Scripts/EnemyAIController.cs
index 8b89cf0..3516173 100644
--- a/Assets/Scripts/EnemyAIController.cs
+++ b/Assets/Scripts/EnemyAIController.cs
@@ -72,13 +72,13 @@ namespace Northbound
private NetworkVariable _currentState = new NetworkVariable(
EnemyAIState.Idle,
NetworkVariableReadPermission.Everyone,
- NetworkVariableWritePermission.Server
+ NetworkVariableWritePermission.Owner
);
private NetworkVariable _targetPlayerId = new NetworkVariable(
0,
NetworkVariableReadPermission.Everyone,
- NetworkVariableWritePermission.Server
+ NetworkVariableWritePermission.Owner
);
private GameObject _cachedTargetPlayer;
@@ -91,7 +91,7 @@ namespace Northbound
_enemyUnit = GetComponent();
_originPosition = transform.position;
- if (IsServer)
+ if (IsOwner)
{
// NavMeshAgent 초기 설정
_agent.speed = moveSpeed;
@@ -121,7 +121,7 @@ namespace Northbound
private void Update()
{
- if (!IsServer) return;
+ if (!IsOwner) return;
if (!_agent.isOnNavMesh) return;
switch (_currentState.Value)
diff --git a/Assets/Scripts/EnemyUnit.cs b/Assets/Scripts/EnemyUnit.cs
index 1319744..16f7d73 100644
--- a/Assets/Scripts/EnemyUnit.cs
+++ b/Assets/Scripts/EnemyUnit.cs
@@ -36,13 +36,21 @@ namespace Northbound
{
base.OnNetworkSpawn();
- if (IsServer)
+ if (IsOwner)
{
- _currentHealth.Value = maxHealth;
- _team.Value = enemyTeam;
+ InitializeServerRpc(maxHealth, enemyTeam);
}
}
+ [ServerRpc]
+ private void InitializeServerRpc(int health, TeamType team)
+ {
+ if (_currentHealth.Value == 0)
+ _currentHealth.Value = health;
+ if (_team.Value == TeamType.Neutral)
+ _team.Value = team;
+ }
+
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
@@ -52,7 +60,18 @@ namespace Northbound
public void TakeDamage(int damage, ulong attackerId)
{
- if (!IsServer) return;
+ if (!IsOwner)
+ {
+ TakeDamageServerRpc(damage, attackerId);
+ return;
+ }
+
+ TakeDamageServerRpc(damage, attackerId);
+ }
+
+ [ServerRpc]
+ private void TakeDamageServerRpc(int damage, ulong attackerId)
+ {
if (_currentHealth.Value <= 0) return;
// 공격자의 팀 확인
@@ -128,7 +147,18 @@ namespace Northbound
public void SetTeam(TeamType team)
{
- if (!IsServer) return;
+ if (!IsOwner)
+ {
+ SetTeamServerRpc(team);
+ return;
+ }
+
+ SetTeamServerRpc(team);
+ }
+
+ [ServerRpc]
+ private void SetTeamServerRpc(TeamType team)
+ {
_team.Value = team;
}
diff --git a/Assets/Scripts/EquipmentSocket.cs b/Assets/Scripts/EquipmentSocket.cs
index a54c3e0..8043381 100644
--- a/Assets/Scripts/EquipmentSocket.cs
+++ b/Assets/Scripts/EquipmentSocket.cs
@@ -1,29 +1,31 @@
using UnityEngine;
using System.Collections.Generic;
+using Unity.Netcode;
namespace Northbound
{
- ///
- /// 플레이어의 장비 소켓 관리 (손, 등, 허리 등)
- ///
- public class EquipmentSocket : MonoBehaviour
+ public class EquipmentSocket : NetworkBehaviour
{
[System.Serializable]
public class Socket
{
- public string socketName; // "RightHand", "LeftHand", "Back" 등
- public Transform socketTransform; // 실제 본 Transform
- [HideInInspector] public GameObject currentEquipment; // 현재 장착된 장비
+ public string socketName;
+ public Transform socketTransform;
+ [HideInInspector] public GameObject currentEquipment;
}
[Header("Available Sockets")]
public List sockets = new List();
+ [Header("Equipment Prefabs")]
+ public GameObject[] equipmentPrefabs;
+
private Dictionary _socketDict = new Dictionary();
+ private Dictionary _prefabDict = new Dictionary();
private void Awake()
{
- // 빠른 검색을 위한 딕셔너리 생성
+ _socketDict.Clear();
foreach (var socket in sockets)
{
if (!string.IsNullOrEmpty(socket.socketName))
@@ -31,82 +33,124 @@ namespace Northbound
_socketDict[socket.socketName] = socket;
}
}
- }
- ///
- /// 소켓에 장비 부착
- ///
- public GameObject AttachToSocket(string socketName, GameObject equipmentPrefab)
- {
- if (!_socketDict.TryGetValue(socketName, out Socket socket))
+ _prefabDict.Clear();
+ if (equipmentPrefabs != null)
{
- Debug.LogWarning($"소켓을 찾을 수 없습니다: {socketName}");
- return null;
- }
-
- if (socket.socketTransform == null)
- {
- Debug.LogWarning($"소켓 Transform이 없습니다: {socketName}");
- return null;
- }
-
- // 기존 장비 제거
- DetachFromSocket(socketName);
-
- // 새 장비 생성
- if (equipmentPrefab != null)
- {
- GameObject equipment = Instantiate(equipmentPrefab, socket.socketTransform);
- equipment.transform.localPosition = Vector3.zero;
- equipment.transform.localRotation = Quaternion.identity;
- socket.currentEquipment = equipment;
-
- return equipment;
- }
-
- return null;
- }
-
- ///
- /// 소켓에서 장비 제거
- ///
- public void DetachFromSocket(string socketName)
- {
- if (!_socketDict.TryGetValue(socketName, out Socket socket))
- return;
-
- if (socket.currentEquipment != null)
- {
- Destroy(socket.currentEquipment);
- socket.currentEquipment = null;
+ foreach (var prefab in equipmentPrefabs)
+ {
+ if (prefab != null)
+ {
+ _prefabDict[prefab.name] = prefab;
+ }
+ }
}
}
- ///
- /// 모든 소켓에서 장비 제거
- ///
- public void DetachAll()
+ public override void OnNetworkDespawn()
{
foreach (var socket in sockets)
{
if (socket.currentEquipment != null)
{
- Destroy(socket.currentEquipment);
+ Object.Destroy(socket.currentEquipment);
socket.currentEquipment = null;
}
}
+ base.OnNetworkDespawn();
+ }
+
+ public GameObject AttachToSocket(string socketName, GameObject equipmentPrefab)
+ {
+ if (equipmentPrefab != null)
+ {
+ AttachToSocketServerRpc(socketName, equipmentPrefab.name);
+ }
+ return null;
+ }
+
+ [Rpc(SendTo.Server)]
+ private void AttachToSocketServerRpc(string socketName, string prefabName)
+ {
+ AttachToSocketClientRpc(socketName, prefabName);
+ }
+
+ [Rpc(SendTo.ClientsAndHost)]
+ private void AttachToSocketClientRpc(string socketName, string prefabName)
+ {
+ if (!_socketDict.ContainsKey(socketName))
+ return;
+
+ var socket = sockets.Find(s => s.socketName == socketName);
+ if (socket == null || socket.socketTransform == null)
+ return;
+
+ DetachFromSocketInternal(socketName);
+
+ GameObject prefab = FindPrefab(prefabName);
+ if (prefab != null)
+ {
+ GameObject equipment = Object.Instantiate(prefab, socket.socketTransform);
+ equipment.transform.localPosition = Vector3.zero;
+ equipment.transform.localRotation = Quaternion.identity;
+ socket.currentEquipment = equipment;
+ }
+ }
+
+ public void DetachFromSocket(string socketName)
+ {
+ DetachFromSocketServerRpc(socketName);
+ }
+
+ [Rpc(SendTo.Server)]
+ private void DetachFromSocketServerRpc(string socketName)
+ {
+ DetachFromSocketClientRpc(socketName);
+ }
+
+ [Rpc(SendTo.ClientsAndHost)]
+ private void DetachFromSocketClientRpc(string socketName)
+ {
+ DetachFromSocketInternal(socketName);
+ }
+
+ private void DetachFromSocketInternal(string socketName)
+ {
+ var socket = sockets.Find(s => s.socketName == socketName);
+ if (socket == null) return;
+
+ if (socket.currentEquipment != null)
+ {
+ Object.Destroy(socket.currentEquipment);
+ socket.currentEquipment = null;
+ }
}
- ///
- /// 특정 소켓에 장비가 있는지 확인
- ///
public bool HasEquipment(string socketName)
{
- if (_socketDict.TryGetValue(socketName, out Socket socket))
+ var socket = sockets.Find(s => s.socketName == socketName);
+ return socket != null && socket.currentEquipment != null;
+ }
+
+ public GameObject GetEquipment(string socketName)
+ {
+ var socket = sockets.Find(s => s.socketName == socketName);
+ return socket != null ? socket.currentEquipment : null;
+ }
+
+ private GameObject FindPrefab(string name)
+ {
+ if (_prefabDict.TryGetValue(name, out var prefab))
{
- return socket.currentEquipment != null;
+ return prefab;
}
- return false;
+
+ prefab = Resources.Load($"Prefabs/{name}");
+ if (prefab != null)
+ {
+ return prefab;
+ }
+ return Resources.Load(name);
}
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Scripts/FogOfWarSystem.cs b/Assets/Scripts/FogOfWarSystem.cs
index a49f540..07376c3 100644
--- a/Assets/Scripts/FogOfWarSystem.cs
+++ b/Assets/Scripts/FogOfWarSystem.cs
@@ -258,7 +258,7 @@ namespace Northbound
private void OnClientConnected(ulong clientId)
{
- if (!IsServer) return;
+ if (!IsOwner) return;
// Ensure fog data exists for this client
if (!_serverFogData.ContainsKey(clientId))
@@ -280,7 +280,7 @@ namespace Northbound
private void Update()
{
- if (!IsServer) return;
+ if (!IsOwner) return;
_updateTimer += Time.deltaTime;
if (_updateTimer >= updateInterval)
diff --git a/Assets/Scripts/GameConstants.cs b/Assets/Scripts/GameConstants.cs
new file mode 100644
index 0000000..3c5bcf8
--- /dev/null
+++ b/Assets/Scripts/GameConstants.cs
@@ -0,0 +1,34 @@
+namespace Northbound
+{
+ public static class GameConstants
+ {
+ public static class Building
+ {
+ public const float BoundsShrinkAmount = 0.01f;
+ public const float MaxPlacementDistance = 100f;
+ public const int MaxDragBuildingCount = 50;
+ }
+
+ public static class Physics
+ {
+ public const float GroundRaycastOriginHeight = 10f;
+ public const float GroundRaycastDistance = 20f;
+ }
+
+ public static class FogOfWar
+ {
+ public const float DefaultUpdateInterval = 0.2f;
+ }
+
+ public static class Player
+ {
+ public const float RespawnDelay = 3f;
+ public const int DefaultMaxHealth = 100;
+ }
+
+ public static class Material
+ {
+ public const int TransparentRenderQueue = 3000;
+ }
+ }
+}
diff --git a/Assets/Scripts/GameConstants.cs.meta b/Assets/Scripts/GameConstants.cs.meta
new file mode 100644
index 0000000..52c9a3d
--- /dev/null
+++ b/Assets/Scripts/GameConstants.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 55b873d6b94628148b3c61c4f5d435fc
\ No newline at end of file
diff --git a/Assets/Scripts/GameResourceUI.cs b/Assets/Scripts/GameResourceUI.cs
index 2d8d985..b5b82f5 100644
--- a/Assets/Scripts/GameResourceUI.cs
+++ b/Assets/Scripts/GameResourceUI.cs
@@ -133,7 +133,7 @@ namespace Northbound
return;
}
- var inventory = localPlayer.GetComponent();
+ var inventory = localPlayer.GetComponent();
if (inventory == null)
{
playerResourceText.text = playerPrefix + "---";
diff --git a/Assets/Scripts/GlobalTimer.cs b/Assets/Scripts/GlobalTimer.cs
index f392a04..237f6c4 100644
--- a/Assets/Scripts/GlobalTimer.cs
+++ b/Assets/Scripts/GlobalTimer.cs
@@ -67,7 +67,7 @@ namespace Northbound
{
base.OnNetworkSpawn();
- if (IsServer && autoStart)
+ if (IsOwner && autoStart)
{
StartTimer();
}
@@ -83,7 +83,7 @@ namespace Northbound
private void Update()
{
- if (!IsServer || !_isRunning.Value)
+ if (!IsOwner || !_isRunning.Value)
return;
_currentTime.Value -= Time.deltaTime;
@@ -149,7 +149,7 @@ namespace Northbound
private void OnCurrentTimeChanged(float previousValue, float newValue)
{
// 클라이언트에서도 Tick 이벤트 발생
- if (!IsServer)
+ if (!IsOwner)
{
OnTimerTick?.Invoke(newValue);
}
@@ -162,7 +162,7 @@ namespace Northbound
///
public void StartTimer()
{
- if (!IsServer) return;
+ if (!IsOwner) return;
_currentTime.Value = cycleLength;
_isRunning.Value = true;
@@ -181,7 +181,7 @@ namespace Northbound
///
public void PauseTimer()
{
- if (!IsServer) return;
+ if (!IsOwner) return;
_isRunning.Value = false;
@@ -194,7 +194,7 @@ namespace Northbound
///
public void ResumeTimer()
{
- if (!IsServer) return;
+ if (!IsOwner) return;
_isRunning.Value = true;
@@ -207,7 +207,7 @@ namespace Northbound
///
public void ResetTimer()
{
- if (!IsServer) return;
+ if (!IsOwner) return;
_currentTime.Value = cycleLength;
_cycleCount.Value = 0;
@@ -255,7 +255,7 @@ namespace Northbound
[ClientRpc]
private void NotifyCycleCompleteClientRpc()
{
- if (!IsServer)
+ if (!IsOwner)
{
OnCycleComplete?.Invoke();
}
@@ -264,7 +264,7 @@ namespace Northbound
[ClientRpc]
private void NotifyCycleStartClientRpc(int cycleNumber)
{
- if (!IsServer)
+ if (!IsOwner)
{
OnCycleStart?.Invoke(cycleNumber);
}
@@ -273,7 +273,7 @@ namespace Northbound
[ClientRpc]
private void NotifyHalfwayClientRpc()
{
- if (!IsServer)
+ if (!IsOwner)
{
OnHalfwayPoint?.Invoke(cycleLength / 2f);
}
diff --git a/Assets/Scripts/InputActionManager.cs b/Assets/Scripts/InputActionManager.cs
new file mode 100644
index 0000000..411c8b2
--- /dev/null
+++ b/Assets/Scripts/InputActionManager.cs
@@ -0,0 +1,54 @@
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.InputSystem;
+
+namespace Northbound
+{
+ public abstract class InputActionManager : NetworkBehaviour
+ {
+ protected PlayerInputActions _inputActions;
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ if (!IsOwner) return;
+
+ InitializeInputActions();
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ if (IsOwner && _inputActions != null)
+ {
+ UnbindInputActions();
+ _inputActions.Disable();
+ _inputActions.Dispose();
+ _inputActions = null;
+ }
+
+ base.OnNetworkDespawn();
+ }
+
+ public override void OnDestroy()
+ {
+ if (_inputActions != null)
+ {
+ _inputActions.Dispose();
+ _inputActions = null;
+ }
+ base.OnDestroy();
+ }
+
+ protected virtual void InitializeInputActions()
+ {
+ _inputActions = new PlayerInputActions();
+ _inputActions.Enable();
+ BindInputActions();
+ }
+
+ protected abstract void BindInputActions();
+
+ protected abstract void UnbindInputActions();
+ }
+}
diff --git a/Assets/Scripts/InputActionManager.cs.meta b/Assets/Scripts/InputActionManager.cs.meta
new file mode 100644
index 0000000..0ae11bb
--- /dev/null
+++ b/Assets/Scripts/InputActionManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 06c90984b0af66542bd5011748fe67c1
\ No newline at end of file
diff --git a/Assets/Scripts/NetworkConnectionHandler.cs b/Assets/Scripts/NetworkConnectionHandler.cs
index 569659a..25dc955 100644
--- a/Assets/Scripts/NetworkConnectionHandler.cs
+++ b/Assets/Scripts/NetworkConnectionHandler.cs
@@ -68,16 +68,11 @@ namespace Northbound
NetworkManager.ConnectionApprovalRequest request,
NetworkManager.ConnectionApprovalResponse response)
{
- // 🔍 디버깅: 스폰 포인트 상태 확인
- if (spawnPoints.Count == 0)
- {
- Debug.LogError($"[Connection] 스폰 포인트가 없습니다! 씬에 PlayerSpawnPoint가 있는지 확인하세요.");
- }
-
response.Approved = true;
response.CreatePlayerObject = true;
-
- // 스폰 위치 설정
+
+ // AutoSpawnPlayerPrefabClientSide를 false로 설정해야 서버에서 스폰 위치가 적용됩니다
+ // 임시로 스폰 위치 설정
response.Position = GetSpawnPosition(request.ClientNetworkId);
response.Rotation = GetSpawnRotation(request.ClientNetworkId);
diff --git a/Assets/Scripts/NetworkManagerUI.cs.meta b/Assets/Scripts/NetworkManagerUI.cs.meta
index 04623a5..74754fe 100644
--- a/Assets/Scripts/NetworkManagerUI.cs.meta
+++ b/Assets/Scripts/NetworkManagerUI.cs.meta
@@ -1,11 +1,2 @@
fileFormatVersion: 2
-guid: 8d7e6f5c4b3a2d1e0f9a8b7c6d5e4f3a
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
+guid: 8d7e6f5c4b3a2d1e0f9a8b7c6d5e4f3a
\ No newline at end of file
diff --git a/Assets/Scripts/NetworkPlayerController.cs b/Assets/Scripts/NetworkPlayerController.cs
index 49dcc90..c2ccefd 100644
--- a/Assets/Scripts/NetworkPlayerController.cs
+++ b/Assets/Scripts/NetworkPlayerController.cs
@@ -1,337 +1,343 @@
using Unity.Netcode;
using UnityEngine;
-using UnityEngine.InputSystem;
using Unity.Cinemachine;
-using Northbound;
-public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageable
+namespace Northbound
{
- [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;
- private Animator _animator;
-
- void Awake()
+ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageable
{
- _controller = GetComponent();
- _animator = GetComponent();
- }
+ [Header("Movement Settings")]
+ public float moveSpeed = 5f;
+ public float rotationSpeed = 10f;
- public override void OnNetworkSpawn()
- {
- base.OnNetworkSpawn();
+ [Header("Team Settings")]
+ [SerializeField] private TeamType initialTeam = TeamType.Player;
- // 서버에서 초기화
- if (IsServer)
+ [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;
+ private Animator _animator;
+
+ void Awake()
+ {
+ _controller = GetComponent();
+ _animator = GetComponent();
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ Debug.Log($"[Player] {gameObject.name} spawned. OwnerId: {OwnerClientId}, LocalClientId: {NetworkManager.Singleton.LocalClientId}, IsOwner: {IsOwner}, IsServer: {IsServer}");
+
+ if (IsOwner)
+ {
+ SetSpawnPosition();
+ InitializePlayerServerRpc(initialTeam, maxHealth);
+ }
+
+ _currentHealth.OnValueChanged += OnHealthChanged;
+
+ if (!IsOwner) return;
+
+ var vcam = GameObject.FindFirstObjectByType();
+
+ if (vcam != null)
+ {
+ vcam.Follow = transform;
+ vcam.LookAt = transform;
+ Debug.Log("[Camera] Camera attached to local player.");
+ }
+
+ _inputActions = new PlayerInputActions();
+ _inputActions.Enable();
+ Debug.Log("[Player] Input actions enabled for local player.");
+ }
+
+ [ServerRpc]
+ private void InitializePlayerServerRpc(TeamType team, int health)
{
if (_team.Value == TeamType.Neutral)
- {
- _team.Value = initialTeam;
- }
-
+ _team.Value = team;
if (_currentHealth.Value == 0)
+ _currentHealth.Value = health;
+
+ Debug.Log($"[Player] {gameObject.name} initialized (Team: {TeamManager.GetTeamName(_team.Value)}, HP: {_currentHealth.Value}/{maxHealth})");
+ }
+
+ private void SetSpawnPosition()
+ {
+ if (PlayerSpawnPositionSetter.Instance == null)
{
- _currentHealth.Value = maxHealth;
+ Debug.LogWarning("[Player] PlayerSpawnPositionSetter not found. Using default spawn position.");
+ return;
}
- Debug.Log($"[Player] {gameObject.name} 스폰됨 (팀: {TeamManager.GetTeamName(_team.Value)}, 체력: {_currentHealth.Value}/{maxHealth})");
+ Vector3 spawnPos = PlayerSpawnPositionSetter.Instance.GetSpawnPosition(OwnerClientId);
+ Quaternion spawnRot = PlayerSpawnPositionSetter.Instance.GetSpawnRotation(OwnerClientId);
+
+ transform.position = spawnPos;
+ transform.rotation = spawnRot;
+
+ Debug.Log($"[Player] Spawn position set: {spawnPos}");
}
- // 체력 변경 이벤트 구독
- _currentHealth.OnValueChanged += OnHealthChanged;
-
- if (!IsOwner) return;
-
- var vcam = GameObject.FindFirstObjectByType();
-
- if (vcam != null)
+ public override void OnNetworkDespawn()
{
- vcam.Follow = transform;
- vcam.LookAt = transform;
- Debug.Log("[Camera] 로컬 플레이어에게 카메라가 연결되었습니다.");
- }
+ _currentHealth.OnValueChanged -= OnHealthChanged;
- _inputActions = new PlayerInputActions();
- _inputActions.Enable();
- }
-
- 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;
-
- // 액션/상호작용 중이면 이동 불가
- var attackAction = GetComponent();
- var playerInteraction = GetComponent();
-
- bool isActionBlocked = (attackAction != null && attackAction.IsAttacking) ||
- (playerInteraction != null && playerInteraction.IsInteracting);
-
- if (isActionBlocked)
- {
- // 이동 불가 시 애니메이션 속도를 0으로
- if (_animator != null)
+ if (IsOwner && _inputActions != null)
{
- _animator.SetFloat("MoveSpeed", 0f);
+ _inputActions.Disable();
+ _inputActions.Dispose();
}
- return;
+
+ base.OnNetworkDespawn();
}
- _moveInput = _inputActions.Player.Move.ReadValue();
- Vector3 move = new Vector3(_moveInput.x, 0, _moveInput.y).normalized;
-
- if (move.magnitude >= 0.1f)
+ void Update()
{
- Quaternion targetRotation = Quaternion.LookRotation(move);
- transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
+ if (!IsOwner) return;
- if (_controller != null)
+ if (_currentHealth.Value <= 0) return;
+
+ var attackAction = GetComponent();
+ var playerInteraction = GetComponent();
+
+ bool isActionBlocked = (attackAction != null && attackAction.IsAttacking) ||
+ (playerInteraction != null && playerInteraction.IsInteracting);
+
+ if (isActionBlocked)
{
- _controller.Move(move * moveSpeed * Time.deltaTime);
- }
- }
-
- if (_animator != null)
- {
- _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))
+ if (_animator != null)
{
- Debug.Log($"[Player] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀을 공격할 수 없습니다.");
- return;
+ _animator.SetFloat("MoveSpeed", 0f);
+ }
+ return;
+ }
+
+ _moveInput = _inputActions.Player.Move.ReadValue();
+ Vector3 move = new Vector3(_moveInput.x, 0, _moveInput.y).normalized;
+
+ if (move.magnitude >= 0.1f)
+ {
+ Quaternion targetRotation = Quaternion.LookRotation(move);
+ transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
+
+ if (_controller != null)
+ {
+ _controller.Move(move * moveSpeed * Time.deltaTime);
}
}
+
+ if (_animator != null)
+ {
+ _animator.SetFloat("MoveSpeed", move.magnitude);
+ }
}
- // 데미지 적용
- int actualDamage = Mathf.Min(damage, _currentHealth.Value);
- _currentHealth.Value -= actualDamage;
+ #region ITeamMember Implementation
- Debug.Log($"[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}");
+ public TeamType GetTeam() => _team.Value;
- // 데미지 이펙트
- ShowDamageEffectClientRpc();
-
- // 체력이 0이 되면 사망
- if (_currentHealth.Value <= 0)
+ public void SetTeam(TeamType team)
{
- Die(attackerId);
+ if (!IsOwner) return;
+
+ SetTeamServerRpc(team);
}
- }
- private void Die(ulong killerId)
- {
- if (!IsServer) return;
-
- Debug.Log($"[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) 사망했습니다! (킬러: {killerId})");
-
- // 사망 이펙트
- ShowDeathEffectClientRpc();
-
- // 애니메이션 (있는 경우)
- if (_animator != null)
+ [ServerRpc]
+ private void SetTeamServerRpc(TeamType team)
{
- _animator.SetTrigger("Die");
+ TeamType previousTeam = _team.Value;
+ _team.Value = team;
+ Debug.Log($"[Player] 팀 변경: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(team)}");
}
- // 일정 시간 후 리스폰 또는 디스폰
- Invoke(nameof(HandleDeath), 3f);
- }
+ #endregion
- private void HandleDeath()
- {
- if (!IsServer) return;
+ #region IDamageable Implementation
- // 여기서 리스폰 로직을 추가하거나 게임 오버 처리
- // 예: 리스폰 위치로 이동 및 체력 회복
- Respawn();
- }
-
- private void Respawn()
- {
- if (!IsServer) return;
-
- // 체력 회복
- _currentHealth.Value = maxHealth;
-
- // 스폰 포인트로 이동 (PlayerSpawnPoint 활용)
- var spawnPoints = FindObjectsByType(FindObjectsSortMode.None);
- if (spawnPoints.Length > 0)
+ public void TakeDamage(int damage, ulong attackerId)
{
- var spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
- transform.position = spawnPoint.transform.position;
- transform.rotation = spawnPoint.transform.rotation;
+ if (!IsOwner)
+ {
+ TakeDamageServerRpc(damage, attackerId);
+ return;
+ }
+
+ TakeDamageServerRpc(damage, attackerId);
}
- Debug.Log($"[Player] {gameObject.name} 리스폰!");
- }
-
- [ClientRpc]
- private void ShowDamageEffectClientRpc()
- {
- if (damageEffectPrefab != null)
+ [ServerRpc]
+ private void TakeDamageServerRpc(int damage, ulong attackerId)
{
- GameObject effect = Instantiate(damageEffectPrefab, transform.position + Vector3.up, Quaternion.identity);
- Destroy(effect, 2f);
- }
- }
+ if (_currentHealth.Value <= 0) return;
- [ClientRpc]
- private void ShowDeathEffectClientRpc()
- {
- if (deathEffectPrefab != null)
+ 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();
+
+ if (_currentHealth.Value <= 0)
+ {
+ Die(attackerId);
+ }
+ }
+
+ private void Die(ulong killerId)
{
- GameObject effect = Instantiate(deathEffectPrefab, transform.position, Quaternion.identity);
- Destroy(effect, 3f);
+ Debug.Log($"[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) 사망했습니다! (킬러: {killerId})");
+
+ ShowDeathEffectClientRpc();
+
+ if (_animator != null)
+ {
+ _animator.SetTrigger("Die");
+ }
+
+ Invoke(nameof(HandleDeath), 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)
+ private void HandleDeath()
{
- // UI 업데이트 등
+ RespawnServerRpc();
}
- }
- #endregion
-
- #region Gizmos
-
- private void OnDrawGizmosSelected()
- {
- #if UNITY_EDITOR
- if (Application.isPlaying)
+ [ServerRpc]
+ private void RespawnServerRpc()
{
- 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
- }
+ _currentHealth.Value = maxHealth;
- #endregion
+ 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;
+
+ public float GetHealthPercentage()
+ {
+ return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f;
+ }
+
+ public bool IsDead() => _currentHealth.Value <= 0;
+
+ public void Heal(int amount)
+ {
+ if (!IsOwner)
+ {
+ HealServerRpc(amount);
+ return;
+ }
+
+ HealServerRpc(amount);
+ }
+
+ [ServerRpc]
+ private void HealServerRpc(int amount)
+ {
+ 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)
+ {
+ Debug.Log($"[Player] 체력 변경: {previousValue} → {newValue}");
+ }
+
+ #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/NetworkPlayerController.cs.meta b/Assets/Scripts/NetworkPlayerController.cs.meta
index fc5a6a8..00e9ed5 100644
--- a/Assets/Scripts/NetworkPlayerController.cs.meta
+++ b/Assets/Scripts/NetworkPlayerController.cs.meta
@@ -1,11 +1,2 @@
fileFormatVersion: 2
-guid: 9e8f7d6c5b4a3d2e1f0a9b8c7d6e5f4a
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
+guid: 19b1385c0dcb77240b2cfb3c6b10f717
\ No newline at end of file
diff --git a/Assets/Scripts/NetworkSpawnHelper.cs b/Assets/Scripts/NetworkSpawnHelper.cs
new file mode 100644
index 0000000..f172c6f
--- /dev/null
+++ b/Assets/Scripts/NetworkSpawnHelper.cs
@@ -0,0 +1,55 @@
+using Unity.Netcode;
+using UnityEngine;
+
+namespace Northbound
+{
+ public static class NetworkSpawnHelper
+ {
+ public static void AddFogOfWarVisibility(GameObject obj, bool showInExploredAreas = true, float updateInterval = 0.2f)
+ {
+ if (obj.GetComponent() == null)
+ {
+ var visibility = obj.AddComponent();
+ visibility.showInExploredAreas = showInExploredAreas;
+ visibility.updateInterval = updateInterval;
+ }
+ }
+
+ public static void PreparePreviewForRendering(GameObject preview)
+ {
+ NetworkObject netObj = preview.GetComponent();
+ if (netObj != null)
+ {
+ Object.DestroyImmediate(netObj);
+ }
+
+ Building building = preview.GetComponent();
+ if (building != null)
+ {
+ Object.DestroyImmediate(building);
+ }
+ }
+
+ public static void ApplyMaterialToPreview(GameObject preview, Material material)
+ {
+ Renderer[] renderers = preview.GetComponentsInChildren();
+ foreach (var renderer in renderers)
+ {
+ Material[] mats = new Material[renderer.materials.Length];
+ for (int i = 0; i < mats.Length; i++)
+ {
+ mats[i] = material;
+ }
+ renderer.materials = mats;
+ }
+ }
+
+ public static void DisableColliders(GameObject obj)
+ {
+ foreach (var collider in obj.GetComponentsInChildren())
+ {
+ collider.enabled = false;
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/NetworkSpawnHelper.cs.meta b/Assets/Scripts/NetworkSpawnHelper.cs.meta
new file mode 100644
index 0000000..b6b47ed
--- /dev/null
+++ b/Assets/Scripts/NetworkSpawnHelper.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9b6c0b0cbea70604fac8231195b6b7b4
\ No newline at end of file
diff --git a/Assets/Scripts/NetworkSpawnManager.cs b/Assets/Scripts/NetworkSpawnManager.cs
index 23b3526..bc01b84 100644
--- a/Assets/Scripts/NetworkSpawnManager.cs
+++ b/Assets/Scripts/NetworkSpawnManager.cs
@@ -16,9 +16,9 @@ namespace Northbound
public List spawnPoints = new List();
public bool useRandomSpawn = false;
public bool findSpawnPointsAutomatically = true;
-
+
private Dictionary _clientSpawnIndices = new Dictionary();
- private int _nextSpawnIndex = 0;
+ private NetworkVariable networkNextSpawnIndex = new NetworkVariable(0);
private void Awake()
{
@@ -100,8 +100,8 @@ namespace Northbound
{
if (!_clientSpawnIndices.ContainsKey(clientId))
{
- _clientSpawnIndices[clientId] = _nextSpawnIndex;
- _nextSpawnIndex = (_nextSpawnIndex + 1) % spawnPoints.Count;
+ _clientSpawnIndices[clientId] = networkNextSpawnIndex.Value;
+ networkNextSpawnIndex.Value = (networkNextSpawnIndex.Value + 1) % spawnPoints.Count;
}
spawnIndex = _clientSpawnIndices[clientId];
}
diff --git a/Assets/Scripts/PlayerActionSystem.cs b/Assets/Scripts/PlayerActionSystem.cs
index 4759507..51acf69 100644
--- a/Assets/Scripts/PlayerActionSystem.cs
+++ b/Assets/Scripts/PlayerActionSystem.cs
@@ -5,10 +5,7 @@ using System.Collections.Generic;
namespace Northbound
{
- ///
- /// 상호작용 대상 없이 실행 가능한 액션들을 관리
- ///
- public class PlayerActionSystem : NetworkBehaviour
+ public class PlayerActionSystem : InputActionManager
{
[Header("Actions")]
public List actionComponents = new List();
@@ -16,7 +13,6 @@ namespace Northbound
[Header("Animation")]
public bool playAnimations = true;
- private PlayerInputActions _inputActions;
private Dictionary _actions = new Dictionary();
private Animator _animator;
@@ -27,9 +23,10 @@ namespace Northbound
public override void OnNetworkSpawn()
{
+ base.OnNetworkSpawn();
+
if (!IsOwner) return;
- // 액션 컴포넌트들을 딕셔너리에 등록
foreach (var component in actionComponents)
{
if (component is IAction action)
@@ -37,21 +34,16 @@ namespace Northbound
_actions[action.GetActionName()] = action;
}
}
-
- _inputActions = new PlayerInputActions();
- _inputActions.Player.Attack.performed += OnAttack;
- // 다른 액션들도 여기에 바인딩
- _inputActions.Enable();
}
- public override void OnNetworkDespawn()
+ protected override void BindInputActions()
{
- if (IsOwner && _inputActions != null)
- {
- _inputActions.Player.Attack.performed -= OnAttack;
- _inputActions.Disable();
- _inputActions.Dispose();
- }
+ _inputActions.Player.Attack.performed += OnAttack;
+ }
+
+ protected override void UnbindInputActions()
+ {
+ _inputActions.Player.Attack.performed -= OnAttack;
}
private void OnAttack(InputAction.CallbackContext context)
@@ -65,7 +57,6 @@ namespace Northbound
{
if (action.CanExecute(OwnerClientId))
{
- // 애니메이션 재생 (액션 실행 전)
if (playAnimations && _animator != null)
{
string animTrigger = action.GetActionAnimation();
@@ -79,15 +70,5 @@ namespace Northbound
}
}
}
-
- override public void OnDestroy()
- {
- if (_inputActions != null)
- {
- _inputActions.Dispose();
- }
-
- base.OnDestroy();
- }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Scripts/PlayerController.cs.meta b/Assets/Scripts/PlayerController.cs.meta
index f86b8ed..68c3895 100644
--- a/Assets/Scripts/PlayerController.cs.meta
+++ b/Assets/Scripts/PlayerController.cs.meta
@@ -1,11 +1,2 @@
fileFormatVersion: 2
-guid: 7a3e5b8c4d2f1a9e6b0c3d7e8f1a2b3c
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
+guid: 7a3e5b8c4d2f1a9e6b0c3d7e8f1a2b3c
\ No newline at end of file
diff --git a/Assets/Scripts/PlayerInteraction.cs b/Assets/Scripts/PlayerInteraction.cs
index 59c6f7a..a6162ba 100644
--- a/Assets/Scripts/PlayerInteraction.cs
+++ b/Assets/Scripts/PlayerInteraction.cs
@@ -27,231 +27,170 @@ namespace Northbound
[Header("Debug")]
public bool showDebugRay = true;
-
+
private PlayerInputActions _inputActions;
private IInteractable _currentInteractable;
private Camera _mainCamera;
private Animator _animator;
private EquipmentSocket _equipmentSocket;
-
- private EquipmentData _pendingEquipmentData;
- private string _currentEquipmentSocket;
- private bool _isInteracting = false;
+ private bool _isInteracting;
+ private float _interactionStartTime;
+ private const float INTERACTION_TIMEOUT = 3f;
- // 다른 컴포넌트가 이동 차단 여부를 확인할 수 있도록 public 프로퍼티 제공
public bool IsInteracting => _isInteracting;
-
- public override void OnNetworkSpawn()
+
+ private void Awake()
{
- if (!IsOwner) return;
-
- _mainCamera = Camera.main;
_animator = GetComponent();
_equipmentSocket = GetComponent();
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
if (rayOrigin == null)
rayOrigin = transform;
-
+
_inputActions = new PlayerInputActions();
_inputActions.Player.Interact.performed += OnInteract;
_inputActions.Enable();
}
-
+
public override void OnNetworkDespawn()
{
- if (IsOwner && _inputActions != null)
+ if (_inputActions != null)
{
_inputActions.Player.Interact.performed -= OnInteract;
_inputActions.Disable();
_inputActions.Dispose();
}
+
+ base.OnNetworkDespawn();
}
-
+
private void Update()
{
if (!IsOwner) return;
+
+ if (_isInteracting && Time.time - _interactionStartTime > INTERACTION_TIMEOUT)
+ {
+ _isInteracting = false;
+ }
+
DetectInteractable();
}
-
+
private void DetectInteractable()
{
Vector3 origin = rayOrigin.position;
Vector3 direction = useForwardDirection ? transform.forward : _mainCamera.transform.forward;
Ray ray = new Ray(origin, direction);
-
+
if (showDebugRay)
{
Debug.DrawRay(ray.origin, ray.direction * interactionRange,
_currentInteractable != null ? Color.green : Color.yellow);
}
-
+
if (Physics.Raycast(ray, out RaycastHit hit, interactionRange, interactableLayer))
{
IInteractable interactable = hit.collider.GetComponent();
if (interactable == null)
+ {
interactable = hit.collider.GetComponentInParent();
-
+ }
+
if (interactable != null && interactable.CanInteract(OwnerClientId))
{
_currentInteractable = interactable;
return;
}
}
-
+
_currentInteractable = null;
}
-
+
private void OnInteract(InputAction.CallbackContext context)
{
- if (blockDuringAnimation && _isInteracting)
+ if (blockDuringAnimation && IsInteracting)
+ return;
+
+ if (_currentInteractable == null)
return;
- if (_currentInteractable != null)
+ var equipmentData = _currentInteractable.GetEquipmentData();
+ string animTrigger = _currentInteractable.GetInteractionAnimation();
+ bool hasAnimation = !string.IsNullOrEmpty(animTrigger);
+
+ if (playAnimations && _animator != null && hasAnimation)
{
_isInteracting = true;
- _pendingEquipmentData = _currentInteractable.GetEquipmentData();
+ _interactionStartTime = Time.time;
- string animTrigger = _currentInteractable.GetInteractionAnimation();
- bool hasAnimation = !string.IsNullOrEmpty(animTrigger);
-
- // 장비 장착 (애니메이션 이벤트 사용 안 할 경우)
- if (!useAnimationEvents && useEquipment && _equipmentSocket != null && _pendingEquipmentData != null)
- {
- if (_pendingEquipmentData.attachOnStart && _pendingEquipmentData.equipmentPrefab != null)
- {
- AttachEquipment();
-
- if (_pendingEquipmentData.detachOnEnd)
- {
- StartCoroutine(DetachEquipmentAfterDelay(2f));
- }
- }
- }
-
- // 애니메이션 재생
- if (playAnimations && _animator != null && hasAnimation)
+ if (useAnimationEvents)
{
_animator.SetTrigger(animTrigger);
}
else
{
- // 애니메이션이 없으면 즉시 상호작용 완료
+ _animator.SetTrigger(animTrigger);
+ _currentInteractable.Interact(OwnerClientId);
_isInteracting = false;
}
-
- // 상호작용 실행 (서버에서 처리)
+ }
+ else
+ {
_currentInteractable.Interact(OwnerClientId);
}
}
-
- // ========================================
- // Animation Event 함수들
- // ========================================
-
- public void OnEquipTool()
+
+ public void AttachEquipment()
{
- if (!useAnimationEvents || !useEquipment) return;
- AttachEquipment();
- }
+ if (!IsOwner) return;
+ if (_currentInteractable == null || !useEquipment) return;
- public void OnEquipTool(string socketName)
+ var equipmentData = _currentInteractable.GetEquipmentData();
+ if (equipmentData == null || !equipmentData.attachOnStart) return;
+
+ if (_equipmentSocket == null)
+ {
+ Debug.LogWarning("[PlayerInteraction] EquipmentSocket component not found on player");
+ return;
+ }
+
+ if (equipmentData.equipmentPrefab == null)
+ {
+ Debug.LogWarning("[PlayerInteraction] Equipment prefab is null. Assign a prefab in the Resource's EquipmentData in Inspector");
+ return;
+ }
+
+ _equipmentSocket.AttachToSocket(equipmentData.socketName, equipmentData.equipmentPrefab);
+ }
+
+ public void DetachEquipment()
{
- if (!useAnimationEvents || !useEquipment) return;
- AttachEquipment(socketName);
- }
+ if (!IsOwner) return;
+ if (_currentInteractable == null || !useEquipment) return;
- public void OnUnequipTool()
- {
- if (!useAnimationEvents || !useEquipment) return;
- DetachEquipment();
- }
+ var equipmentData = _currentInteractable.GetEquipmentData();
+ if (equipmentData == null || !equipmentData.detachOnEnd) return;
- public void OnUnequipTool(string socketName)
- {
- if (!useAnimationEvents || !useEquipment) return;
- DetachEquipment(socketName);
- }
+ if (_equipmentSocket == null) return;
+ _equipmentSocket.DetachFromSocket(equipmentData.socketName);
+ }
+
public void OnInteractionComplete()
{
_isInteracting = false;
- Debug.Log("[PlayerInteraction] 상호작용 완료");
- }
- // ========================================
- // 내부 헬퍼 함수들
- // ========================================
-
- private void AttachEquipment(string socketName = null)
- {
- if (_equipmentSocket == null || _pendingEquipmentData == null)
- return;
-
- if (_pendingEquipmentData.equipmentPrefab == null)
- return;
-
- string socket = socketName ?? _pendingEquipmentData.socketName;
- _equipmentSocket.AttachToSocket(socket, _pendingEquipmentData.equipmentPrefab);
- _currentEquipmentSocket = socket;
- }
-
- private void DetachEquipment(string socketName = null)
- {
- if (_equipmentSocket == null)
- return;
-
- string socket = socketName ?? _currentEquipmentSocket;
-
- if (!string.IsNullOrEmpty(socket))
+ if (_currentInteractable != null)
{
- _equipmentSocket.DetachFromSocket(socket);
-
- if (socket == _currentEquipmentSocket)
- _currentEquipmentSocket = null;
- }
- }
-
- private System.Collections.IEnumerator DetachEquipmentAfterDelay(float delay)
- {
- yield return new WaitForSeconds(delay);
- DetachEquipment();
-
- if (!useAnimationEvents)
- {
- _isInteracting = false;
- }
- }
-
- private void OnGUI()
- {
- if (!IsOwner || _currentInteractable == null) return;
-
- GUIStyle style = new GUIStyle(GUI.skin.label)
- {
- fontSize = 24,
- alignment = TextAnchor.MiddleCenter
- };
- style.normal.textColor = Color.white;
-
- string prompt = _currentInteractable.GetInteractionPrompt();
-
- if (_isInteracting)
- {
- prompt += " (진행 중...)";
- style.normal.textColor = Color.yellow;
- }
-
- GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 100, 400, 50), prompt, style);
- }
-
- public override void OnDestroy()
- {
- if (_inputActions != null)
- {
- _inputActions.Dispose();
+ _currentInteractable.Interact(OwnerClientId);
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Scripts/PlayerInventory.cs b/Assets/Scripts/PlayerInventory.cs
new file mode 100644
index 0000000..108f243
--- /dev/null
+++ b/Assets/Scripts/PlayerInventory.cs
@@ -0,0 +1,73 @@
+using Unity.Netcode;
+using UnityEngine;
+
+namespace Northbound
+{
+ public class PlayerInventory : NetworkBehaviour
+ {
+ [Header("Inventory Settings")]
+ public int maxResourceCapacity = 100;
+
+ private NetworkVariable resourceCount = new NetworkVariable(
+ 0,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Owner
+ );
+
+ public int CurrentResourceAmount => resourceCount.Value;
+ public int MaxResourceCapacity => maxResourceCapacity;
+
+ public bool CanAddResource(int amount)
+ {
+ return resourceCount.Value + amount <= maxResourceCapacity;
+ }
+
+ public int GetAvailableSpace()
+ {
+ return maxResourceCapacity - resourceCount.Value;
+ }
+
+ public void AddResources(int amount)
+ {
+ if (amount <= 0) return;
+
+ int actualAmount = Mathf.Min(amount, maxResourceCapacity - resourceCount.Value);
+ resourceCount.Value += actualAmount;
+
+ Debug.Log($"Player {OwnerClientId} added resources: +{actualAmount}, total: {resourceCount.Value}/{maxResourceCapacity}");
+ }
+
+ public void RemoveResources(int amount)
+ {
+ if (amount <= 0) return;
+
+ int actualAmount = Mathf.Min(amount, resourceCount.Value);
+ resourceCount.Value -= actualAmount;
+
+ Debug.Log($"Player {OwnerClientId} used resources: -{actualAmount}, total: {resourceCount.Value}/{maxResourceCapacity}");
+ }
+
+ public void SetResources(int amount)
+ {
+ resourceCount.Value = Mathf.Clamp(amount, 0, maxResourceCapacity);
+ }
+
+ [Rpc(SendTo.Owner)]
+ public void AddResourcesRpc(int amount)
+ {
+ AddResources(amount);
+ }
+
+ [Rpc(SendTo.Owner)]
+ public void RemoveResourcesRpc(int amount)
+ {
+ RemoveResources(amount);
+ }
+
+ [Rpc(SendTo.Owner)]
+ public void SetResourcesRpc(int amount)
+ {
+ SetResources(amount);
+ }
+ }
+}
diff --git a/Assets/Scripts/PlayerInventory.cs.meta b/Assets/Scripts/PlayerInventory.cs.meta
new file mode 100644
index 0000000..24dd336
--- /dev/null
+++ b/Assets/Scripts/PlayerInventory.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 90231c209cbef84469511de397004be9
\ No newline at end of file
diff --git a/Assets/Scripts/PlayerResourceInventory.cs b/Assets/Scripts/PlayerResourceInventory.cs
deleted file mode 100644
index 63b41ff..0000000
--- a/Assets/Scripts/PlayerResourceInventory.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using Unity.Netcode;
-using UnityEngine;
-
-namespace Northbound
-{
- ///
- /// 플레이어의 자원 인벤토리 관리
- ///
- public class PlayerResourceInventory : NetworkBehaviour
- {
- [Header("Inventory Settings")]
- public int maxResourceCapacity = 100; // 최대 자원 보유량
-
- private NetworkVariable _currentResourceAmount = new NetworkVariable(
- 0,
- NetworkVariableReadPermission.Everyone,
- NetworkVariableWritePermission.Server
- );
-
- public int CurrentResourceAmount => _currentResourceAmount.Value;
- public int MaxResourceCapacity => maxResourceCapacity;
-
- ///
- /// 자원을 추가할 수 있는지 확인
- ///
- public bool CanAddResource(int amount)
- {
- return _currentResourceAmount.Value + amount <= maxResourceCapacity;
- }
-
- ///
- /// 추가 가능한 최대 자원량 계산
- ///
- public int GetAvailableSpace()
- {
- return maxResourceCapacity - _currentResourceAmount.Value;
- }
-
- ///
- /// 자원 추가 (서버에서만 호출)
- ///
- [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
- public void AddResourceServerRpc(int amount)
- {
- if (amount <= 0) return;
-
- int actualAmount = Mathf.Min(amount, maxResourceCapacity - _currentResourceAmount.Value);
- _currentResourceAmount.Value += actualAmount;
-
- Debug.Log($"플레이어 {OwnerClientId} - 자원 추가: +{actualAmount}, 현재: {_currentResourceAmount.Value}/{maxResourceCapacity}");
- }
-
- ///
- /// 자원 제거 (서버에서만 호출)
- ///
- [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
- public void RemoveResourceServerRpc(int amount)
- {
- if (amount <= 0) return;
-
- int actualAmount = Mathf.Min(amount, _currentResourceAmount.Value);
- _currentResourceAmount.Value -= actualAmount;
-
- Debug.Log($"플레이어 {OwnerClientId} - 자원 사용: -{actualAmount}, 현재: {_currentResourceAmount.Value}/{maxResourceCapacity}");
- }
-
- ///
- /// 자원 설정 (서버에서만 호출)
- ///
- [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
- public void SetResourceServerRpc(int amount)
- {
- _currentResourceAmount.Value = Mathf.Clamp(amount, 0, maxResourceCapacity);
- }
- }
-}
\ No newline at end of file
diff --git a/Assets/Scripts/PlayerResourceInventory.cs.meta b/Assets/Scripts/PlayerResourceInventory.cs.meta
deleted file mode 100644
index 5984f85..0000000
--- a/Assets/Scripts/PlayerResourceInventory.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 3c64072402b0a3f46a674eb73c5541ac
\ No newline at end of file
diff --git a/Assets/Scripts/PlayerSpawnPositionSetter.cs b/Assets/Scripts/PlayerSpawnPositionSetter.cs
new file mode 100644
index 0000000..b6159b3
--- /dev/null
+++ b/Assets/Scripts/PlayerSpawnPositionSetter.cs
@@ -0,0 +1,103 @@
+using System.Collections.Generic;
+using System.Linq;
+using Unity.Netcode;
+using UnityEngine;
+
+namespace Northbound
+{
+ public class PlayerSpawnPositionSetter : NetworkBehaviour
+ {
+ public static PlayerSpawnPositionSetter Instance { get; private set; }
+
+ [Header("Spawn Settings")]
+ public List spawnPoints = new List();
+ public bool useRandomSpawn = false;
+ public bool findSpawnPointsAutomatically = true;
+
+ private void Awake()
+ {
+ if (Instance != null && Instance != this)
+ {
+ Destroy(gameObject);
+ return;
+ }
+ Instance = this;
+
+ if (findSpawnPointsAutomatically)
+ {
+ FindSpawnPoints();
+ }
+ }
+
+ private void FindSpawnPoints()
+ {
+ PlayerSpawnPoint[] points = FindObjectsByType(FindObjectsSortMode.None);
+ var sortedPoints = points.OrderBy(p => p.spawnIndex == -1 ? int.MaxValue : p.spawnIndex);
+
+ spawnPoints.Clear();
+ foreach (var point in sortedPoints)
+ {
+ if (point.isAvailable)
+ {
+ spawnPoints.Add(point.transform);
+ }
+ }
+
+ Debug.Log($"[SpawnPositionSetter] {spawnPoints.Count}개의 스폰 포인트를 찾았습니다.");
+ }
+
+ public Vector3 GetSpawnPosition(ulong clientId)
+ {
+ if (spawnPoints.Count == 0)
+ {
+ Debug.LogWarning("[SpawnPositionSetter] 스폰 포인트가 없습니다. 기본 위치 반환.");
+ return Vector3.zero;
+ }
+
+ int spawnIndex = GetSpawnIndexForClient(clientId);
+
+ Debug.Log($"[SpawnPositionSetter] 클라이언트 {clientId}에게 스폰 인덱스 {spawnIndex} 할당");
+ return spawnPoints[spawnIndex].position;
+ }
+
+ private int GetSpawnIndexForClient(ulong clientId)
+ {
+ if (useRandomSpawn)
+ {
+ return Random.Range(0, spawnPoints.Count);
+ }
+
+ int spawnIndex;
+ if (IsServer)
+ {
+ spawnIndex = GetAssignedSpawnIndexServer(clientId);
+ }
+ else
+ {
+ spawnIndex = (int)(clientId % (ulong)spawnPoints.Count);
+ }
+
+ return spawnIndex;
+ }
+
+ private int GetAssignedSpawnIndexServer(ulong clientId)
+ {
+ List connectedClientIds = new List(NetworkManager.Singleton.ConnectedClientsIds);
+ connectedClientIds.Sort();
+
+ int index = connectedClientIds.IndexOf(clientId);
+ if (index < 0) index = 0;
+
+ return index % spawnPoints.Count;
+ }
+
+ public Quaternion GetSpawnRotation(ulong clientId)
+ {
+ if (spawnPoints.Count == 0)
+ return Quaternion.identity;
+
+ int spawnIndex = GetSpawnIndexForClient(clientId);
+ return spawnPoints[spawnIndex].rotation;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/PlayerSpawnPositionSetter.cs.meta b/Assets/Scripts/PlayerSpawnPositionSetter.cs.meta
new file mode 100644
index 0000000..3c48721
--- /dev/null
+++ b/Assets/Scripts/PlayerSpawnPositionSetter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 49cd9c4e7c611b04c8740c9e049129b9
\ No newline at end of file
diff --git a/Assets/Scripts/Resource.cs b/Assets/Scripts/Resource.cs
index f79b949..2c43535 100644
--- a/Assets/Scripts/Resource.cs
+++ b/Assets/Scripts/Resource.cs
@@ -45,7 +45,7 @@ namespace Northbound
public override void OnNetworkSpawn()
{
- if (IsServer)
+ if (IsOwner)
{
_currentResources.Value = maxResources;
_lastRechargeTime = Time.time;
@@ -54,7 +54,7 @@ namespace Northbound
private void Update()
{
- if (!IsServer)
+ if (!IsOwner)
return;
// 자원 충전 로직
@@ -88,7 +88,7 @@ namespace Northbound
{
if (client.PlayerObject != null)
{
- var playerInventory = client.PlayerObject.GetComponent();
+ var playerInventory = client.PlayerObject.GetComponent();
if (playerInventory != null)
{
// 플레이어가 받을 수 있는 공간이 없으면 상호작용 불가
@@ -120,10 +120,10 @@ namespace Northbound
if (playerObject == null)
return;
- var playerInventory = playerObject.GetComponent();
+ var playerInventory = playerObject.GetComponent();
if (playerInventory == null)
{
- Debug.LogWarning($"플레이어 {playerId}에게 PlayerResourceInventory 컴포넌트가 없습니다.");
+ Debug.LogWarning($"플레이어 {playerId}에게 PlayerInventory 컴포넌트가 없습니다.");
return;
}
@@ -147,8 +147,8 @@ namespace Northbound
_currentResources.Value -= gatheredAmount;
_lastGatheringTime = Time.time;
- // 플레이어에게 자원 추가
- playerInventory.AddResourceServerRpc(gatheredAmount);
+ // 플레이어에게 자원 추가 (RPC로 owner에게 요청)
+ playerInventory.AddResourcesRpc(gatheredAmount);
Debug.Log($"플레이어 {playerId}가 {gatheredAmount} {resourceName}을(를) 채집했습니다. 남은 자원: {_currentResources.Value}");
diff --git a/Assets/Scripts/ResourcePickup.cs b/Assets/Scripts/ResourcePickup.cs
index 2a21fee..519148d 100644
--- a/Assets/Scripts/ResourcePickup.cs
+++ b/Assets/Scripts/ResourcePickup.cs
@@ -42,7 +42,7 @@ namespace Northbound
{
if (client.PlayerObject != null)
{
- var playerInventory = client.PlayerObject.GetComponent();
+ var playerInventory = client.PlayerObject.GetComponent();
if (playerInventory != null)
{
// 플레이어가 받을 수 있는 공간이 없으면 상호작용 불가
@@ -80,10 +80,10 @@ namespace Northbound
if (playerObject == null)
return;
- var playerInventory = playerObject.GetComponent();
+ var playerInventory = playerObject.GetComponent();
if (playerInventory == null)
{
- Debug.LogWarning($"플레이어 {playerId}에게 PlayerResourceInventory 컴포넌트가 없습니다.");
+ Debug.LogWarning($"플레이어 {playerId}에게 PlayerInventory 컴포넌트가 없습니다.");
return;
}
@@ -100,8 +100,8 @@ namespace Northbound
return;
}
- // 플레이어에게 자원 추가
- playerInventory.AddResourceServerRpc(collectedAmount);
+ // 플레이어에게 자원 추가 (RPC로 owner에게 요청)
+ playerInventory.AddResourcesRpc(collectedAmount);
Debug.Log($"플레이어 {playerId}가 {collectedAmount} {resourceName}을(를) 획득했습니다.");
diff --git a/Assets/Scripts/SmartAutoHost.cs b/Assets/Scripts/SmartAutoHost.cs
new file mode 100644
index 0000000..3b70a3c
--- /dev/null
+++ b/Assets/Scripts/SmartAutoHost.cs
@@ -0,0 +1,52 @@
+using UnityEngine;
+using Unity.Netcode;
+
+public class SmartAutoHost : MonoBehaviour
+{
+#if UNITY_EDITOR
+ private void Start()
+ {
+ if (NetworkManager.Singleton == null)
+ {
+ Debug.LogError("[SmartAutoHost] NetworkManager not found!");
+ return;
+ }
+
+ if (NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsClient)
+ {
+ return;
+ }
+
+ bool isMainEditor = IsMainEditor();
+
+ if (isMainEditor)
+ {
+ NetworkManager.Singleton.StartHost();
+ Debug.Log("[SmartAutoHost] MAIN EDITOR → Starting as HOST");
+ }
+ else
+ {
+ NetworkManager.Singleton.StartClient();
+ Debug.Log("[SmartAutoHost] SECONDARY EDITOR → Connecting as CLIENT");
+ }
+ }
+
+ private bool IsMainEditor()
+ {
+ string[] args = System.Environment.GetCommandLineArgs();
+ return System.Array.Exists(args, arg => arg == "-mainEditor");
+ }
+#else
+ private void Start()
+ {
+ // 빌드된 버전은 항상 클라이언트
+ if (NetworkManager.Singleton != null &&
+ !NetworkManager.Singleton.IsServer &&
+ !NetworkManager.Singleton.IsClient)
+ {
+ NetworkManager.Singleton.StartClient();
+ Debug.Log("[SmartAutoHost] Build → Connecting as CLIENT");
+ }
+ }
+#endif
+}
diff --git a/Assets/Scripts/SmartAutoHost.cs.meta b/Assets/Scripts/SmartAutoHost.cs.meta
new file mode 100644
index 0000000..cbe9936
--- /dev/null
+++ b/Assets/Scripts/SmartAutoHost.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 13b6e5c1ebfd6fb4d939d2e99b25fffc
\ No newline at end of file
diff --git a/Assets/Scripts/TeamGate.cs b/Assets/Scripts/TeamGate.cs
index b5f1249..512e27d 100644
--- a/Assets/Scripts/TeamGate.cs
+++ b/Assets/Scripts/TeamGate.cs
@@ -97,7 +97,7 @@ namespace Northbound
private void OnTriggerEnter(Collider other)
{
// 서버에서만 텔레포트 처리
- if (!IsServer) return;
+ if (!IsOwner) return;
// 어느 트리거에 진입했는지 확인
Collider triggeredCollider = null;
diff --git a/Assets/Scripts/TeamMemberNetworkBehaviour.cs b/Assets/Scripts/TeamMemberNetworkBehaviour.cs
new file mode 100644
index 0000000..0fc486b
--- /dev/null
+++ b/Assets/Scripts/TeamMemberNetworkBehaviour.cs
@@ -0,0 +1,62 @@
+using Unity.Netcode;
+using UnityEngine;
+
+namespace Northbound
+{
+ public abstract class TeamMemberNetworkBehaviour : NetworkBehaviour, ITeamMember
+ {
+ [SerializeField] protected TeamType initialTeam = TeamType.Player;
+
+ protected NetworkVariable _team = new NetworkVariable(
+ TeamType.Neutral,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ public event System.Action OnTeamChanged;
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ if (IsOwner)
+ {
+ if (_team.Value == TeamType.Neutral)
+ {
+ _team.Value = initialTeam;
+ }
+ }
+
+ _team.OnValueChanged += OnTeamValueChanged;
+ UpdateTeamVisuals();
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ _team.OnValueChanged -= OnTeamValueChanged;
+ base.OnNetworkDespawn();
+ }
+
+ public TeamType GetTeam() => _team.Value;
+
+ public void SetTeam(TeamType team)
+ {
+ if (!IsOwner) return;
+
+ TeamType previousTeam = _team.Value;
+ _team.Value = team;
+
+ Debug.Log($"[{GetType().Name}] {gameObject.name} team changed: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(team)}");
+ }
+
+ protected virtual void OnTeamValueChanged(TeamType previousValue, TeamType newValue)
+ {
+ OnTeamChanged?.Invoke(newValue);
+ UpdateTeamVisuals();
+ }
+
+ protected virtual void UpdateTeamVisuals()
+ {
+ }
+ }
+}
diff --git a/Assets/Scripts/TeamMemberNetworkBehaviour.cs.meta b/Assets/Scripts/TeamMemberNetworkBehaviour.cs.meta
new file mode 100644
index 0000000..d536936
--- /dev/null
+++ b/Assets/Scripts/TeamMemberNetworkBehaviour.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 25aae83d83e1df146ad1b22af79848bb
\ No newline at end of file
diff --git a/Assets/UserChoices.choices b/Assets/UserChoices.choices
new file mode 100644
index 0000000..b32598c
--- /dev/null
+++ b/Assets/UserChoices.choices
@@ -0,0 +1,36 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &1
+MonoBehaviour:
+ m_ObjectHideFlags: 53
+ 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: 18cde282a8d045bf9d245fdcfaa7271b, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Multiplayer.Center.Editor::Unity.Multiplayer.Center.Questionnaire.UserChoicesObject
+ QuestionnaireVersion: 1.3
+ UserAnswers:
+ Answers:
+ - QuestionId: Pace
+ Answers:
+ - Fast
+ - QuestionId: Cheating
+ Answers:
+ - CheatingNotImportant
+ - QuestionId: CostSensitivity
+ Answers:
+ - BestMargin
+ - QuestionId: NetcodeArchitecture
+ Answers:
+ - ClientServer
+ - QuestionId: PlayerCount
+ Answers:
+ - 4
+ Preset: 11
+ SelectedSolutions:
+ SelectedHostingModel: 4
+ SelectedNetcodeSolution: 1
diff --git a/Assets/UserChoices.choices.meta b/Assets/UserChoices.choices.meta
new file mode 100644
index 0000000..8a1afbc
--- /dev/null
+++ b/Assets/UserChoices.choices.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 624b6b15c0f5b4c4d912ee2e880c8581
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/ExternAttributes.Editor.csproj b/ExternAttributes.Editor.csproj
index d74c257..ac29e91 100644
--- a/ExternAttributes.Editor.csproj
+++ b/ExternAttributes.Editor.csproj
@@ -43,7 +43,6 @@
6000.3.5f2
-
@@ -631,6 +630,10 @@
C:\Program Files\Unity\Hub\Editor\6000.3.5f2\Editor\Data\PlaybackEngines\WindowsStandaloneSupport\UnityEditor.WindowsStandalone.Extensions.dll
False
+
+ Library\PackageCache\com.unity.services.wire@9a73acde80cc\Plugins\websocket-sharp.dll
+ False
+
Library\PackageCache\com.unity.visualscripting@191c0d7e3b69\Editor\VisualScripting.Core\Dependencies\YamlDotNet\Unity.VisualScripting.YamlDotNet.dll
False
diff --git a/FlatKit.Utils.Editor.csproj b/FlatKit.Utils.Editor.csproj
index 598b48b..3e99887 100644
--- a/FlatKit.Utils.Editor.csproj
+++ b/FlatKit.Utils.Editor.csproj
@@ -43,7 +43,6 @@
6000.3.5f2
-
@@ -614,6 +613,10 @@
C:\Program Files\Unity\Hub\Editor\6000.3.5f2\Editor\Data\PlaybackEngines\WindowsStandaloneSupport\UnityEditor.WindowsStandalone.Extensions.dll
False
+
+ Library\PackageCache\com.unity.services.wire@9a73acde80cc\Plugins\websocket-sharp.dll
+ False
+
Library\PackageCache\com.unity.visualscripting@191c0d7e3b69\Editor\VisualScripting.Core\Dependencies\YamlDotNet\Unity.VisualScripting.YamlDotNet.dll
False
diff --git a/Northbound.sln b/Northbound.sln
new file mode 100644
index 0000000..5016e48
--- /dev/null
+++ b/Northbound.sln
@@ -0,0 +1,44 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp", "Assembly-CSharp.csproj", "{5C483496-7666-CA70-B23E-AE514115EE94}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.RenderPipelines.Universal.Runtime", "Unity.RenderPipelines.Universal.Runtime.csproj", "{7D4DAA2B-AB80-6E9C-56F6-9BBE627B864B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlatKit.Utils.Editor", "FlatKit.Utils.Editor.csproj", "{9FFF03DB-13D2-F002-5CE2-72F68DD2B255}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExternAttributes.Editor", "ExternAttributes.Editor.csproj", "{2EE5E6D5-274E-E52C-7FC1-8ABD29E99E2F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-Editor", "Assembly-CSharp-Editor.csproj", "{EF726A86-D8F8-62E9-98E4-841FF7F5D94D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5C483496-7666-CA70-B23E-AE514115EE94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5C483496-7666-CA70-B23E-AE514115EE94}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5C483496-7666-CA70-B23E-AE514115EE94}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5C483496-7666-CA70-B23E-AE514115EE94}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7D4DAA2B-AB80-6E9C-56F6-9BBE627B864B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7D4DAA2B-AB80-6E9C-56F6-9BBE627B864B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7D4DAA2B-AB80-6E9C-56F6-9BBE627B864B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7D4DAA2B-AB80-6E9C-56F6-9BBE627B864B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9FFF03DB-13D2-F002-5CE2-72F68DD2B255}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9FFF03DB-13D2-F002-5CE2-72F68DD2B255}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9FFF03DB-13D2-F002-5CE2-72F68DD2B255}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9FFF03DB-13D2-F002-5CE2-72F68DD2B255}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2EE5E6D5-274E-E52C-7FC1-8ABD29E99E2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2EE5E6D5-274E-E52C-7FC1-8ABD29E99E2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2EE5E6D5-274E-E52C-7FC1-8ABD29E99E2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2EE5E6D5-274E-E52C-7FC1-8ABD29E99E2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EF726A86-D8F8-62E9-98E4-841FF7F5D94D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EF726A86-D8F8-62E9-98E4-841FF7F5D94D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EF726A86-D8F8-62E9-98E4-841FF7F5D94D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EF726A86-D8F8-62E9-98E4-841FF7F5D94D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/Packages/manifest.json b/Packages/manifest.json
index 5dce0f5..9cfe791 100644
--- a/Packages/manifest.json
+++ b/Packages/manifest.json
@@ -1,5 +1,6 @@
{
"dependencies": {
+ "com.boxqkrtm.ide.cursor": "https://github.com/boxqkrtm/com.unity.ide.cursor.git",
"com.unity.2d.animation": "13.0.4",
"com.unity.2d.aseprite": "3.0.1",
"com.unity.2d.psdimporter": "12.0.1",
@@ -15,9 +16,12 @@
"com.unity.ide.visualstudio": "2.0.26",
"com.unity.inputsystem": "1.17.0",
"com.unity.multiplayer.center": "1.0.1",
+ "com.unity.multiplayer.center.quickstart": "1.1.1",
+ "com.unity.multiplayer.playmode": "2.0.1",
"com.unity.multiplayer.tools": "2.2.7",
- "com.unity.netcode.gameobjects": "2.8.0",
+ "com.unity.netcode.gameobjects": "2.8.1",
"com.unity.render-pipelines.universal": "17.3.0",
+ "com.unity.services.multiplayer": "2.1.0",
"com.unity.test-framework": "1.6.0",
"com.unity.timeline": "1.8.10",
"com.unity.ugui": "2.0.0",
diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json
index 5b2109e..9c5ba18 100644
--- a/Packages/packages-lock.json
+++ b/Packages/packages-lock.json
@@ -1,5 +1,14 @@
{
"dependencies": {
+ "com.boxqkrtm.ide.cursor": {
+ "version": "https://github.com/boxqkrtm/com.unity.ide.cursor.git",
+ "depth": 0,
+ "source": "git",
+ "dependencies": {
+ "com.unity.test-framework": "1.1.9"
+ },
+ "hash": "53e1baac3cde90c46934f82ee1d61802d2bd6651"
+ },
"com.unity.2d.animation": {
"version": "13.0.4",
"depth": 0,
@@ -196,6 +205,25 @@
"com.unity.modules.uielements": "1.0.0"
}
},
+ "com.unity.multiplayer.center.quickstart": {
+ "version": "1.1.1",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.modules.uielements": "1.0.0",
+ "com.unity.multiplayer.center": "1.0.0"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.multiplayer.playmode": {
+ "version": "2.0.1",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.nuget.newtonsoft-json": "2.0.2"
+ },
+ "url": "https://packages.unity.com"
+ },
"com.unity.multiplayer.tools": {
"version": "2.2.7",
"depth": 0,
@@ -212,7 +240,7 @@
"url": "https://packages.unity.com"
},
"com.unity.netcode.gameobjects": {
- "version": "2.8.0",
+ "version": "2.8.1",
"depth": 0,
"source": "registry",
"dependencies": {
@@ -281,6 +309,87 @@
"dependencies": {},
"url": "https://packages.unity.com"
},
+ "com.unity.services.authentication": {
+ "version": "3.6.0",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.ugui": "1.0.0",
+ "com.unity.services.core": "1.15.1",
+ "com.unity.nuget.newtonsoft-json": "3.2.1",
+ "com.unity.modules.unitywebrequest": "1.0.0"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.services.core": {
+ "version": "1.16.0",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.modules.androidjni": "1.0.0",
+ "com.unity.nuget.newtonsoft-json": "3.2.1",
+ "com.unity.modules.unitywebrequest": "1.0.0"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.services.deployment": {
+ "version": "1.6.2",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.services.core": "1.15.1",
+ "com.unity.services.deployment.api": "1.1.2"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.services.deployment.api": {
+ "version": "1.1.3",
+ "depth": 2,
+ "source": "registry",
+ "dependencies": {},
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.services.multiplayer": {
+ "version": "2.1.0",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.transport": "2.5.0",
+ "com.unity.collections": "2.2.1",
+ "com.unity.services.qos": "1.3.0",
+ "com.unity.services.core": "1.15.1",
+ "com.unity.services.wire": "1.4.0",
+ "com.unity.services.deployment": "1.6.2",
+ "com.unity.nuget.newtonsoft-json": "3.2.1",
+ "com.unity.modules.unitywebrequest": "1.0.0",
+ "com.unity.services.authentication": "3.6.0"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.services.qos": {
+ "version": "1.4.1",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.collections": "1.2.4",
+ "com.unity.services.core": "1.12.5",
+ "com.unity.nuget.newtonsoft-json": "3.0.2",
+ "com.unity.modules.unitywebrequest": "1.0.0",
+ "com.unity.services.authentication": "3.5.2"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.services.wire": {
+ "version": "1.4.1",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.services.core": "1.12.5",
+ "com.unity.nuget.newtonsoft-json": "3.2.1",
+ "com.unity.services.authentication": "2.7.4"
+ },
+ "url": "https://packages.unity.com"
+ },
"com.unity.settings-manager": {
"version": "2.1.1",
"depth": 2,
diff --git a/ProjectSettings/VirtualProjectsConfig.json b/ProjectSettings/VirtualProjectsConfig.json
new file mode 100644
index 0000000..a47892c
--- /dev/null
+++ b/ProjectSettings/VirtualProjectsConfig.json
@@ -0,0 +1,4 @@
+{
+ "PlayerTags": [],
+ "version": "6000.3.5f2"
+}
\ No newline at end of file
diff --git a/Unity.RenderPipelines.Universal.Runtime.csproj b/Unity.RenderPipelines.Universal.Runtime.csproj
index 00a650f..6c754ae 100644
--- a/Unity.RenderPipelines.Universal.Runtime.csproj
+++ b/Unity.RenderPipelines.Universal.Runtime.csproj
@@ -43,15 +43,14 @@
6000.3.5f2
-
-
+
@@ -143,8 +142,8 @@
-
+
@@ -247,8 +246,8 @@
-
+
@@ -832,6 +831,10 @@
C:\Program Files\Unity\Hub\Editor\6000.3.5f2\Editor\Data\PlaybackEngines\WindowsStandaloneSupport\UnityEditor.WindowsStandalone.Extensions.dll
False
+
+ Library\PackageCache\com.unity.services.wire@9a73acde80cc\Plugins\websocket-sharp.dll
+ False
+
Library\PackageCache\com.unity.visualscripting@191c0d7e3b69\Editor\VisualScripting.Core\Dependencies\YamlDotNet\Unity.VisualScripting.YamlDotNet.dll
False