feat: 멀티플레이어 네트워크 동기화 구현
- 로비 씬 추가 및 LobbyManager/LobbyUI/LobbySceneBuilder 구현 - NetworkPrefabsList로 플레이어 프리팹 등록 (PlayerPrefab 자동스폰 비활성화) - PlayerMovement 서버 권한 이동 아키텍처로 전환 - NetworkVariable<Vector2>로 클라이언트 입력 → 서버 전달 - 점프 JumpRequestRpc로 서버 검증 후 실행 - 보스 프리팹에 NetworkTransform/NetworkAnimator 추가 (서버 권한) - SkillController를 NetworkBehaviour로 전환 - PlaySkillClipClientRpc로 클립 override + 재생 원자적 동기화 - OnEffect/OnSkillEnd 클라이언트 실행 차단 - WeaponEquipment 클라이언트 무기 시각화 동기화 수정 - registeredWeapons 인덱스 기반 NetworkVariable 동기화 - SpawnWeaponVisualsLocal로 클라이언트 무기 생성 - 중복 Instantiate 버그 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3631
Assets/Scenes/Lobby.unity
Normal file
3631
Assets/Scenes/Lobby.unity
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/Scenes/Lobby.unity.meta
Normal file
7
Assets/Scenes/Lobby.unity.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1999405fa34dae341b5f919ead00585b
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1907,6 +1907,7 @@ MonoBehaviour:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.GameManager
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.GameManager
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
playerPrefab: {fileID: 6473031571298860035, guid: 9f538e60b8b98634b8952310b91dfba0, type: 3}
|
||||||
gameOverUIPrefab: {fileID: 1938792592198155266, guid: e065144df1b7fcc479fd319fb571e171, type: 3}
|
gameOverUIPrefab: {fileID: 1938792592198155266, guid: e065144df1b7fcc479fd319fb571e171, type: 3}
|
||||||
victoryUIPrefab: {fileID: 1938792592198155266, guid: e065144df1b7fcc479fd319fb571e171, type: 3}
|
victoryUIPrefab: {fileID: 1938792592198155266, guid: e065144df1b7fcc479fd319fb571e171, type: 3}
|
||||||
victoryEffectPrefab: {fileID: 0}
|
victoryEffectPrefab: {fileID: 0}
|
||||||
|
|||||||
21
Assets/_Game/NetworkPrefabsList.asset
Normal file
21
Assets/_Game/NetworkPrefabsList.asset
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: e651dbb3fbac04af2b8f5abf007ddc23, type: 3}
|
||||||
|
m_Name: NetworkPrefabsList
|
||||||
|
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkPrefabsList
|
||||||
|
IsDefault: 0
|
||||||
|
List:
|
||||||
|
- Override: 0
|
||||||
|
Prefab: {fileID: 6473031571298860035, guid: 9f538e60b8b98634b8952310b91dfba0, type: 3}
|
||||||
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
|
SourceHashToOverride: 0
|
||||||
|
OverridingTargetPrefab: {fileID: 0}
|
||||||
8
Assets/_Game/NetworkPrefabsList.asset.meta
Normal file
8
Assets/_Game/NetworkPrefabsList.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5320cf0e941c6c044a4c8663f86cd9e2
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -19,6 +19,8 @@ GameObject:
|
|||||||
- component: {fileID: 11400004}
|
- component: {fileID: 11400004}
|
||||||
- component: {fileID: 11400005}
|
- component: {fileID: 11400005}
|
||||||
- component: {fileID: 11400006}
|
- component: {fileID: 11400006}
|
||||||
|
- component: {fileID: 3792588902782784034}
|
||||||
|
- component: {fileID: 9069822911508997612}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: Prefab_Boss_BossTemplate
|
m_Name: Prefab_Boss_BossTemplate
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -243,3 +245,69 @@ MonoBehaviour:
|
|||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
characterStats: {fileID: 0}
|
characterStats: {fileID: 0}
|
||||||
networkController: {fileID: 0}
|
networkController: {fileID: 0}
|
||||||
|
--- !u!114 &3792588902782784034
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 100000}
|
||||||
|
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 &9069822911508997612
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 100000}
|
||||||
|
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: 9500000}
|
||||||
|
TransitionStateInfoList: []
|
||||||
|
AnimatorParameterEntries:
|
||||||
|
ParameterEntries: []
|
||||||
|
AnimatorParametersExpanded: 0
|
||||||
|
|||||||
@@ -1930,6 +1930,8 @@ GameObject:
|
|||||||
- component: {fileID: 8818883032728065057}
|
- component: {fileID: 8818883032728065057}
|
||||||
- component: {fileID: -2857689419101920665}
|
- component: {fileID: -2857689419101920665}
|
||||||
- component: {fileID: 7544406269366897481}
|
- component: {fileID: 7544406269366897481}
|
||||||
|
- component: {fileID: 4137653497738922896}
|
||||||
|
- component: {fileID: 2670434347080613863}
|
||||||
m_Layer: 6
|
m_Layer: 6
|
||||||
m_Name: Prefab_Boss_TestBoss
|
m_Name: Prefab_Boss_TestBoss
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -1998,7 +2000,7 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||||
GlobalObjectIdHash: 860882280
|
GlobalObjectIdHash: 1928582172
|
||||||
InScenePlacedSourceGlobalObjectIdHash: 223369646
|
InScenePlacedSourceGlobalObjectIdHash: 223369646
|
||||||
DeferredDespawnTick: 0
|
DeferredDespawnTick: 0
|
||||||
Ownership: 1
|
Ownership: 1
|
||||||
@@ -2132,9 +2134,18 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 59b4feaa06ce4c74f97ed5b57ddd74d1, type: 3}
|
m_Script: {fileID: 11500000, guid: 59b4feaa06ce4c74f97ed5b57ddd74d1, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillController
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillController
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
animator: {fileID: 4019041888965840580}
|
animator: {fileID: 4019041888965840580}
|
||||||
baseController: {fileID: 9100000, guid: 4bd980f1a222c5b468136f7e717925d5, type: 2}
|
baseController: {fileID: 9100000, guid: 4bd980f1a222c5b468136f7e717925d5, type: 2}
|
||||||
baseSkillClip: {fileID: -7717634560727564301, guid: 4005a77aa7d531742b1de1bec27001b1, type: 3}
|
baseSkillClip: {fileID: -7717634560727564301, guid: 4005a77aa7d531742b1de1bec27001b1, type: 3}
|
||||||
|
registeredClips:
|
||||||
|
- {fileID: -242498254790479478, guid: 585e8961b6c6e9f4ba96bdb4ffb2cbc3, type: 3}
|
||||||
|
- {fileID: 3627526391332626453, guid: 39aaec38fc96c4842b972f1e991e5a46, type: 3}
|
||||||
|
- {fileID: -7717634560727564301, guid: 4005a77aa7d531742b1de1bec27001b1, type: 3}
|
||||||
|
- {fileID: -8265974341663887746, guid: d3e4690f866332b43b86ee7005291cd0, type: 3}
|
||||||
|
- {fileID: 712281148059590495, guid: b590c58b50c3b554687b172862fa5d9d, type: 3}
|
||||||
|
- {fileID: 6888780564265376159, guid: 827dfeae95fdf6b41b78698f2e846b5f, type: 3}
|
||||||
|
- {fileID: -8752051743343580635, guid: 5eaeca917bbeb494eb14ad0e0552c42f, type: 3}
|
||||||
debugMode: 1
|
debugMode: 1
|
||||||
showAreaDebug: 1
|
showAreaDebug: 1
|
||||||
debugDrawDuration: 1
|
debugDrawDuration: 1
|
||||||
@@ -2166,6 +2177,100 @@ MonoBehaviour:
|
|||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
characterStats: {fileID: 0}
|
characterStats: {fileID: 0}
|
||||||
networkController: {fileID: 0}
|
networkController: {fileID: 0}
|
||||||
|
--- !u!114 &4137653497738922896
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 5581648761285601425}
|
||||||
|
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 &2670434347080613863
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 5581648761285601425}
|
||||||
|
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: 4019041888965840580}
|
||||||
|
TransitionStateInfoList: []
|
||||||
|
AnimatorParameterEntries:
|
||||||
|
ParameterEntries:
|
||||||
|
- name: Speed
|
||||||
|
NameHash: -823668238
|
||||||
|
Synchronize: 1
|
||||||
|
ParameterType: 1
|
||||||
|
- name: IsGrounded
|
||||||
|
NameHash: 507951781
|
||||||
|
Synchronize: 1
|
||||||
|
ParameterType: 4
|
||||||
|
- name: Jump
|
||||||
|
NameHash: 125937960
|
||||||
|
Synchronize: 1
|
||||||
|
ParameterType: 9
|
||||||
|
- name: Land
|
||||||
|
NameHash: 137525990
|
||||||
|
Synchronize: 1
|
||||||
|
ParameterType: 9
|
||||||
|
- name: Attack
|
||||||
|
NameHash: 1080829965
|
||||||
|
Synchronize: 1
|
||||||
|
ParameterType: 9
|
||||||
|
- name: Skill
|
||||||
|
NameHash: -1610822797
|
||||||
|
Synchronize: 1
|
||||||
|
ParameterType: 9
|
||||||
|
- name: Die
|
||||||
|
NameHash: 20298039
|
||||||
|
Synchronize: 1
|
||||||
|
ParameterType: 9
|
||||||
|
AnimatorParametersExpanded: 0
|
||||||
--- !u!1 &5887309268024433402
|
--- !u!1 &5887309268024433402
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ MonoBehaviour:
|
|||||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkAnimator
|
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkAnimator
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
NetworkAnimatorExpanded: 0
|
NetworkAnimatorExpanded: 0
|
||||||
AuthorityMode: 0
|
AuthorityMode: 1
|
||||||
m_Animator: {fileID: 3426985706796420257}
|
m_Animator: {fileID: 3426985706796420257}
|
||||||
TransitionStateInfoList: []
|
TransitionStateInfoList: []
|
||||||
AnimatorParameterEntries:
|
AnimatorParameterEntries:
|
||||||
@@ -291,9 +291,11 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 59b4feaa06ce4c74f97ed5b57ddd74d1, type: 3}
|
m_Script: {fileID: 11500000, guid: 59b4feaa06ce4c74f97ed5b57ddd74d1, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillController
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillController
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
animator: {fileID: 3426985706796420257}
|
animator: {fileID: 3426985706796420257}
|
||||||
baseController: {fileID: 9100000, guid: db718381bb2992e469c76c64015e065b, type: 2}
|
baseController: {fileID: 9100000, guid: db718381bb2992e469c76c64015e065b, type: 2}
|
||||||
baseSkillClip: {fileID: -7717634560727564301, guid: 0f6fd9302e489b94d96774e2713b1317, type: 3}
|
baseSkillClip: {fileID: -7717634560727564301, guid: 0f6fd9302e489b94d96774e2713b1317, type: 3}
|
||||||
|
registeredClips: []
|
||||||
debugMode: 1
|
debugMode: 1
|
||||||
showAreaDebug: 1
|
showAreaDebug: 1
|
||||||
debugDrawDuration: 1
|
debugDrawDuration: 1
|
||||||
@@ -393,6 +395,8 @@ MonoBehaviour:
|
|||||||
hipName: Hip
|
hipName: Hip
|
||||||
twoHandedName:
|
twoHandedName:
|
||||||
startingWeapon: {fileID: 11400000, guid: 646964ccbda84e947b97537d7f7813aa, type: 2}
|
startingWeapon: {fileID: 11400000, guid: 646964ccbda84e947b97537d7f7813aa, type: 2}
|
||||||
|
registeredWeapons:
|
||||||
|
- {fileID: 11400000, guid: 646964ccbda84e947b97537d7f7813aa, type: 2}
|
||||||
--- !u!1001 &361239394574523229
|
--- !u!1001 &361239394574523229
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
215
Assets/_Game/Prefabs/UI/UI_PlayerSlot.prefab
Normal file
215
Assets/_Game/Prefabs/UI/UI_PlayerSlot.prefab
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!1 &2261926377149268810
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 8565956399073525088}
|
||||||
|
- component: {fileID: 7526036062879878267}
|
||||||
|
- component: {fileID: 6608519611067429497}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Label
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!224 &8565956399073525088
|
||||||
|
RectTransform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2261926377149268810}
|
||||||
|
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: 1053676444485162300}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
|
m_AnchorMax: {x: 1, y: 1}
|
||||||
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
|
m_SizeDelta: {x: -16, y: 0}
|
||||||
|
m_Pivot: {x: 0.5, y: 0.5}
|
||||||
|
--- !u!222 &7526036062879878267
|
||||||
|
CanvasRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2261926377149268810}
|
||||||
|
m_CullTransparentMesh: 1
|
||||||
|
--- !u!114 &6608519611067429497
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2261926377149268810}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
m_RaycastTarget: 1
|
||||||
|
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
m_Maskable: 1
|
||||||
|
m_OnCullStateChanged:
|
||||||
|
m_PersistentCalls:
|
||||||
|
m_Calls: []
|
||||||
|
m_text: Player
|
||||||
|
m_isRightToLeft: 0
|
||||||
|
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
|
||||||
|
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
|
||||||
|
m_fontSharedMaterials: []
|
||||||
|
m_fontMaterial: {fileID: 0}
|
||||||
|
m_fontMaterials: []
|
||||||
|
m_fontColor32:
|
||||||
|
serializedVersion: 2
|
||||||
|
rgba: 4294967295
|
||||||
|
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
m_enableVertexGradient: 0
|
||||||
|
m_colorMode: 3
|
||||||
|
m_fontColorGradient:
|
||||||
|
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
m_fontColorGradientPreset: {fileID: 0}
|
||||||
|
m_spriteAsset: {fileID: 0}
|
||||||
|
m_tintAllSprites: 0
|
||||||
|
m_StyleSheet: {fileID: 0}
|
||||||
|
m_TextStyleHashCode: 0
|
||||||
|
m_overrideHtmlColors: 0
|
||||||
|
m_faceColor:
|
||||||
|
serializedVersion: 2
|
||||||
|
rgba: 4294967295
|
||||||
|
m_fontSize: 20
|
||||||
|
m_fontSizeBase: 20
|
||||||
|
m_fontWeight: 400
|
||||||
|
m_enableAutoSizing: 0
|
||||||
|
m_fontSizeMin: 18
|
||||||
|
m_fontSizeMax: 72
|
||||||
|
m_fontStyle: 0
|
||||||
|
m_HorizontalAlignment: 1
|
||||||
|
m_VerticalAlignment: 4096
|
||||||
|
m_textAlignment: 65535
|
||||||
|
m_characterSpacing: 0
|
||||||
|
m_characterHorizontalScale: 1
|
||||||
|
m_wordSpacing: 0
|
||||||
|
m_lineSpacing: 0
|
||||||
|
m_lineSpacingMax: 0
|
||||||
|
m_paragraphSpacing: 0
|
||||||
|
m_charWidthMaxAdj: 0
|
||||||
|
m_TextWrappingMode: 1
|
||||||
|
m_wordWrappingRatios: 0.4
|
||||||
|
m_overflowMode: 0
|
||||||
|
m_linkedTextComponent: {fileID: 0}
|
||||||
|
parentLinkedComponent: {fileID: 0}
|
||||||
|
m_enableKerning: 0
|
||||||
|
m_ActiveFontFeatures: 6e72656b
|
||||||
|
m_enableExtraPadding: 0
|
||||||
|
checkPaddingRequired: 0
|
||||||
|
m_isRichText: 1
|
||||||
|
m_EmojiFallbackSupport: 1
|
||||||
|
m_parseCtrlCharacters: 1
|
||||||
|
m_isOrthographic: 1
|
||||||
|
m_isCullingEnabled: 0
|
||||||
|
m_horizontalMapping: 0
|
||||||
|
m_verticalMapping: 0
|
||||||
|
m_uvLineOffset: 0
|
||||||
|
m_geometrySortingOrder: 0
|
||||||
|
m_IsTextObjectScaleStatic: 0
|
||||||
|
m_VertexBufferAutoSizeReduction: 0
|
||||||
|
m_useMaxVisibleDescender: 1
|
||||||
|
m_pageToDisplay: 1
|
||||||
|
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
m_isUsingLegacyAnimationComponent: 0
|
||||||
|
m_isVolumetricText: 0
|
||||||
|
m_hasFontAssetChanged: 0
|
||||||
|
m_baseMaterial: {fileID: 0}
|
||||||
|
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
--- !u!1 &5513989657015937982
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1053676444485162300}
|
||||||
|
- component: {fileID: 2619735212364195173}
|
||||||
|
- component: {fileID: 5129437633899893877}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: PlayerSlot
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!224 &1053676444485162300
|
||||||
|
RectTransform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 5513989657015937982}
|
||||||
|
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:
|
||||||
|
- {fileID: 8565956399073525088}
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||||
|
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||||
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
|
m_SizeDelta: {x: 380, y: 40}
|
||||||
|
m_Pivot: {x: 0.5, y: 0.5}
|
||||||
|
--- !u!222 &2619735212364195173
|
||||||
|
CanvasRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 5513989657015937982}
|
||||||
|
m_CullTransparentMesh: 1
|
||||||
|
--- !u!114 &5129437633899893877
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 5513989657015937982}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_Color: {r: 0.2, g: 0.2, b: 0.2, a: 0.8}
|
||||||
|
m_RaycastTarget: 1
|
||||||
|
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
m_Maskable: 1
|
||||||
|
m_OnCullStateChanged:
|
||||||
|
m_PersistentCalls:
|
||||||
|
m_Calls: []
|
||||||
|
m_Sprite: {fileID: 0}
|
||||||
|
m_Type: 0
|
||||||
|
m_PreserveAspect: 0
|
||||||
|
m_FillCenter: 1
|
||||||
|
m_FillMethod: 4
|
||||||
|
m_FillAmount: 1
|
||||||
|
m_FillClockwise: 1
|
||||||
|
m_FillOrigin: 0
|
||||||
|
m_UseSpriteMesh: 0
|
||||||
|
m_PixelsPerUnitMultiplier: 1
|
||||||
7
Assets/_Game/Prefabs/UI/UI_PlayerSlot.prefab.meta
Normal file
7
Assets/_Game/Prefabs/UI/UI_PlayerSlot.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b4e86ed3d040269429d9ace4454313f1
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -27,6 +27,10 @@ namespace Colosseum
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class GameManager : NetworkBehaviour
|
public class GameManager : NetworkBehaviour
|
||||||
{
|
{
|
||||||
|
[Header("Player")]
|
||||||
|
[Tooltip("플레이어 프리팹 (NetworkObject 포함)")]
|
||||||
|
[SerializeField] private GameObject playerPrefab;
|
||||||
|
|
||||||
[Header("UI Prefabs")]
|
[Header("UI Prefabs")]
|
||||||
[Tooltip("게임 오버 UI 프리팹")]
|
[Tooltip("게임 오버 UI 프리팹")]
|
||||||
[SerializeField] private GameObject gameOverUIPrefab;
|
[SerializeField] private GameObject gameOverUIPrefab;
|
||||||
@@ -107,33 +111,62 @@ namespace Colosseum
|
|||||||
if (loadSceneMode == LoadSceneMode.Single)
|
if (loadSceneMode == LoadSceneMode.Single)
|
||||||
{
|
{
|
||||||
if (debugMode)
|
if (debugMode)
|
||||||
{
|
|
||||||
Debug.Log($"[GameManager] Scene loaded: {sceneName}");
|
Debug.Log($"[GameManager] Scene loaded: {sceneName}");
|
||||||
}
|
|
||||||
|
|
||||||
// 씬 로드 완료 시 플레이어 리스폰
|
|
||||||
if (IsServer)
|
if (IsServer)
|
||||||
|
StartCoroutine(SpawnPlayersAndRespawn(clientsCompleted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator SpawnPlayersAndRespawn(List<ulong> clientsCompleted)
|
||||||
|
{
|
||||||
|
if (playerPrefab == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[GameManager] playerPrefab이 설정되지 않았습니다. 인스펙터에서 할당하세요.");
|
||||||
|
RespawnAllPlayersClientRpc();
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log($"[GameManager] SpawnPlayersAndRespawn: clientsCompleted=[{string.Join(",", clientsCompleted)}]");
|
||||||
|
|
||||||
|
// 씬 로드를 완료한 클라이언트마다 플레이어 스폰
|
||||||
|
foreach (ulong clientId in clientsCompleted)
|
||||||
|
{
|
||||||
|
var go = Instantiate(playerPrefab);
|
||||||
|
var no = go.GetComponent<NetworkObject>();
|
||||||
|
if (no != null)
|
||||||
{
|
{
|
||||||
RespawnAllPlayersClientRpc();
|
no.SpawnAsPlayerObject(clientId, true);
|
||||||
|
Debug.Log($"[GameManager] Spawned player for clientId={clientId}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError($"[GameManager] playerPrefab에 NetworkObject가 없습니다!");
|
||||||
|
Destroy(go);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 클라이언트가 스폰된 오브젝트를 받을 시간 여유
|
||||||
|
yield return new WaitForSeconds(0.5f);
|
||||||
|
RespawnAllPlayersClientRpc();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Rpc(SendTo.ClientsAndHost)]
|
[Rpc(SendTo.ClientsAndHost)]
|
||||||
private void RespawnAllPlayersClientRpc()
|
private void RespawnAllPlayersClientRpc()
|
||||||
{
|
{
|
||||||
// 모든 플레이어 리스폰
|
// 서버: 모든 플레이어 체력/상태 리셋
|
||||||
var players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
|
var players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
|
||||||
foreach (var player in players)
|
foreach (var player in players)
|
||||||
{
|
{
|
||||||
player.Respawn();
|
player.Respawn();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 카메라 재설정
|
// 카메라 재설정 — 자신이 소유한 플레이어만
|
||||||
var playerMovement = FindObjectsByType<PlayerMovement>(FindObjectsSortMode.None);
|
var playerMovements = FindObjectsByType<PlayerMovement>(FindObjectsSortMode.None);
|
||||||
foreach (var movement in playerMovement)
|
foreach (var movement in playerMovements)
|
||||||
{
|
{
|
||||||
movement.RefreshCamera();
|
if (movement.IsOwner)
|
||||||
|
movement.RefreshCamera();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
415
Assets/_Game/Scripts/Editor/LobbySceneBuilder.cs
Normal file
415
Assets/_Game/Scripts/Editor/LobbySceneBuilder.cs
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using TMPro;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using Unity.Netcode.Transports.UTP;
|
||||||
|
|
||||||
|
namespace Colosseum.Editor
|
||||||
|
{
|
||||||
|
public static class LobbySceneBuilder
|
||||||
|
{
|
||||||
|
[MenuItem("Colosseum/Build Lobby Scene")]
|
||||||
|
public static void Build()
|
||||||
|
{
|
||||||
|
// ── 씬 생성 ──────────────────────────────────────────
|
||||||
|
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
|
|
||||||
|
// ── NetworkManager ───────────────────────────────────
|
||||||
|
var nmGO = new GameObject("NetworkManager");
|
||||||
|
var nm = nmGO.AddComponent<NetworkManager>();
|
||||||
|
var transport = nmGO.AddComponent<UnityTransport>();
|
||||||
|
nm.NetworkConfig = new NetworkConfig
|
||||||
|
{
|
||||||
|
NetworkTransport = transport
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Network Prefabs 등록 ──────────────────────────────
|
||||||
|
var playerPrefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/_Game/Prefabs/Player/Prefab_Player_Default.prefab");
|
||||||
|
if (playerPrefabAsset != null)
|
||||||
|
AddNetworkPrefab(nm, playerPrefabAsset);
|
||||||
|
else
|
||||||
|
Debug.LogWarning("[LobbySceneBuilder] Player prefab not found at expected path.");
|
||||||
|
|
||||||
|
// ── LobbyManager ─────────────────────────────────────
|
||||||
|
var lmGO = new GameObject("LobbyManager");
|
||||||
|
lmGO.AddComponent<Unity.Netcode.NetworkObject>();
|
||||||
|
lmGO.AddComponent<Colosseum.Network.LobbyManager>();
|
||||||
|
|
||||||
|
// ── Canvas ───────────────────────────────────────────
|
||||||
|
var canvasGO = new GameObject("Canvas");
|
||||||
|
var canvas = canvasGO.AddComponent<Canvas>();
|
||||||
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||||
|
canvasGO.AddComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||||
|
canvasGO.AddComponent<GraphicRaycaster>();
|
||||||
|
|
||||||
|
// EventSystem — New Input System 사용 중이므로 InputSystemUIInputModule 사용
|
||||||
|
var esGO = new GameObject("EventSystem");
|
||||||
|
esGO.AddComponent<UnityEngine.EventSystems.EventSystem>();
|
||||||
|
esGO.AddComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
|
||||||
|
|
||||||
|
// ── ConnectPanel ──────────────────────────────────────
|
||||||
|
var connectPanel = CreatePanel(canvasGO.transform, "ConnectPanel");
|
||||||
|
var connectRect = connectPanel.GetComponent<RectTransform>();
|
||||||
|
connectRect.anchorMin = Vector2.zero;
|
||||||
|
connectRect.anchorMax = Vector2.one;
|
||||||
|
connectRect.offsetMin = Vector2.zero;
|
||||||
|
connectRect.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
var vLayout = connectPanel.AddComponent<VerticalLayoutGroup>();
|
||||||
|
vLayout.childAlignment = TextAnchor.MiddleCenter;
|
||||||
|
vLayout.spacing = 12;
|
||||||
|
vLayout.childForceExpandWidth = false;
|
||||||
|
vLayout.childForceExpandHeight = false;
|
||||||
|
connectPanel.AddComponent<ContentSizeFitter>();
|
||||||
|
|
||||||
|
CreateLabel(connectPanel.transform, "TitleLabel", "Colosseum Lobby", 36);
|
||||||
|
var ipInput = CreateInputField(connectPanel.transform, "IpInput", "Host IP (127.0.0.1)", 300, 50);
|
||||||
|
var portInput = CreateInputField(connectPanel.transform, "PortInput", "Port (7777)", 300, 50);
|
||||||
|
var hostBtn = CreateButton(connectPanel.transform, "HostButton", "Host", 200, 50);
|
||||||
|
var joinBtn = CreateButton(connectPanel.transform, "JoinButton", "Join", 200, 50);
|
||||||
|
var statusText = CreateLabel(connectPanel.transform, "StatusText", "", 18);
|
||||||
|
statusText.color = Color.yellow;
|
||||||
|
|
||||||
|
// ── LobbyPanel ────────────────────────────────────────
|
||||||
|
var lobbyPanel = CreatePanel(canvasGO.transform, "LobbyPanel");
|
||||||
|
var lobbyRect = lobbyPanel.GetComponent<RectTransform>();
|
||||||
|
lobbyRect.anchorMin = Vector2.zero;
|
||||||
|
lobbyRect.anchorMax = Vector2.one;
|
||||||
|
lobbyRect.offsetMin = Vector2.zero;
|
||||||
|
lobbyRect.offsetMax = Vector2.zero;
|
||||||
|
lobbyPanel.SetActive(false);
|
||||||
|
|
||||||
|
var vLayout2 = lobbyPanel.AddComponent<VerticalLayoutGroup>();
|
||||||
|
vLayout2.childAlignment = TextAnchor.MiddleCenter;
|
||||||
|
vLayout2.spacing = 12;
|
||||||
|
vLayout2.childForceExpandWidth = false;
|
||||||
|
vLayout2.childForceExpandHeight = false;
|
||||||
|
|
||||||
|
CreateLabel(lobbyPanel.transform, "LobbyTitle", "Waiting Room", 32);
|
||||||
|
|
||||||
|
// PlayerList: ScrollView 역할을 하는 VerticalLayout 컨테이너
|
||||||
|
var playerListGO = new GameObject("PlayerList");
|
||||||
|
playerListGO.transform.SetParent(lobbyPanel.transform, false);
|
||||||
|
var plRect = playerListGO.AddComponent<RectTransform>();
|
||||||
|
plRect.sizeDelta = new Vector2(400, 200);
|
||||||
|
var plLayout = playerListGO.AddComponent<VerticalLayoutGroup>();
|
||||||
|
plLayout.childAlignment = TextAnchor.UpperCenter;
|
||||||
|
plLayout.spacing = 8;
|
||||||
|
plLayout.childForceExpandWidth = true;
|
||||||
|
plLayout.childForceExpandHeight = false;
|
||||||
|
playerListGO.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||||
|
|
||||||
|
var readyBtn = CreateButton(lobbyPanel.transform, "ReadyButton", "준비", 200, 50);
|
||||||
|
var startBtn = CreateButton(lobbyPanel.transform, "StartButton", "게임 시작", 200, 50);
|
||||||
|
var discBtn = CreateButton(lobbyPanel.transform, "DisconnectButton", "나가기", 200, 50);
|
||||||
|
|
||||||
|
// ── PlayerSlot 프리팹 ─────────────────────────────────
|
||||||
|
var slotGO = new GameObject("PlayerSlot");
|
||||||
|
var slotRect = slotGO.AddComponent<RectTransform>();
|
||||||
|
slotRect.sizeDelta = new Vector2(380, 40);
|
||||||
|
var bg = slotGO.AddComponent<Image>();
|
||||||
|
bg.color = new Color(0.2f, 0.2f, 0.2f, 0.8f);
|
||||||
|
|
||||||
|
var slotLabel = new GameObject("Label");
|
||||||
|
slotLabel.transform.SetParent(slotGO.transform, false);
|
||||||
|
var slotLabelRect = slotLabel.AddComponent<RectTransform>();
|
||||||
|
slotLabelRect.anchorMin = Vector2.zero;
|
||||||
|
slotLabelRect.anchorMax = Vector2.one;
|
||||||
|
slotLabelRect.offsetMin = new Vector2(8, 0);
|
||||||
|
slotLabelRect.offsetMax = new Vector2(-8, 0);
|
||||||
|
var slotTmp = slotLabel.AddComponent<TextMeshProUGUI>();
|
||||||
|
slotTmp.text = "Player";
|
||||||
|
slotTmp.fontSize = 20;
|
||||||
|
slotTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||||
|
|
||||||
|
System.IO.Directory.CreateDirectory(Application.dataPath + "/_Game/Prefabs/UI");
|
||||||
|
var slotPrefab = PrefabUtility.SaveAsPrefabAsset(slotGO, "Assets/_Game/Prefabs/UI/PlayerSlot.prefab");
|
||||||
|
Object.DestroyImmediate(slotGO);
|
||||||
|
|
||||||
|
// ── LobbyUI 연결 ──────────────────────────────────────
|
||||||
|
var uiGO = new GameObject("UIController");
|
||||||
|
var lobbyUI = uiGO.AddComponent<Colosseum.UI.LobbyUI>();
|
||||||
|
|
||||||
|
SetPrivateField(lobbyUI, "connectPanel", connectPanel);
|
||||||
|
SetPrivateField(lobbyUI, "ipInput", ipInput.GetComponent<TMP_InputField>());
|
||||||
|
SetPrivateField(lobbyUI, "portInput", portInput.GetComponent<TMP_InputField>());
|
||||||
|
SetPrivateField(lobbyUI, "hostButton", hostBtn.GetComponent<Button>());
|
||||||
|
SetPrivateField(lobbyUI, "joinButton", joinBtn.GetComponent<Button>());
|
||||||
|
SetPrivateField(lobbyUI, "connectStatusText", statusText);
|
||||||
|
SetPrivateField(lobbyUI, "lobbyPanel", lobbyPanel);
|
||||||
|
SetPrivateField(lobbyUI, "playerListParent", playerListGO.transform);
|
||||||
|
SetPrivateField(lobbyUI, "playerSlotPrefab", slotPrefab);
|
||||||
|
SetPrivateField(lobbyUI, "readyButton", readyBtn.GetComponent<Button>());
|
||||||
|
SetPrivateField(lobbyUI, "startButton", startBtn.GetComponent<Button>());
|
||||||
|
SetPrivateField(lobbyUI, "disconnectButton", discBtn.GetComponent<Button>());
|
||||||
|
|
||||||
|
// ── 씬 저장 ───────────────────────────────────────────
|
||||||
|
EditorSceneManager.SaveScene(scene, "Assets/Scenes/Lobby.unity");
|
||||||
|
|
||||||
|
// ── Build Settings 등록 ───────────────────────────────
|
||||||
|
AddSceneToBuildSettings("Assets/Scenes/Lobby.unity", 0);
|
||||||
|
AddSceneToBuildSettings("Assets/Scenes/Test.unity", 1);
|
||||||
|
|
||||||
|
Debug.Log("[LobbySceneBuilder] Lobby scene built successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("Colosseum/Register Network Prefabs")]
|
||||||
|
public static void RegisterNetworkPrefabs()
|
||||||
|
{
|
||||||
|
var nm = Object.FindFirstObjectByType<NetworkManager>();
|
||||||
|
if (nm == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[LobbySceneBuilder] NetworkManager not found in current scene.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerPrefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/_Game/Prefabs/Player/Prefab_Player_Default.prefab");
|
||||||
|
if (playerPrefabAsset == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[LobbySceneBuilder] Player prefab not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkPrefabsList ScriptableObject 생성 또는 로드
|
||||||
|
const string listPath = "Assets/_Game/NetworkPrefabsList.asset";
|
||||||
|
var prefabList = AssetDatabase.LoadAssetAtPath<NetworkPrefabsList>(listPath);
|
||||||
|
if (prefabList == null)
|
||||||
|
{
|
||||||
|
prefabList = ScriptableObject.CreateInstance<NetworkPrefabsList>();
|
||||||
|
AssetDatabase.CreateAsset(prefabList, listPath);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
Debug.Log("[LobbySceneBuilder] Created NetworkPrefabsList asset.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 플레이어 프리팹을 리스트에 추가 (내부 필드명은 "List")
|
||||||
|
if (!prefabList.Contains(playerPrefabAsset))
|
||||||
|
{
|
||||||
|
prefabList.Add(new NetworkPrefab { Prefab = playerPrefabAsset });
|
||||||
|
EditorUtility.SetDirty(prefabList);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
Debug.Log("[LobbySceneBuilder] Added player prefab to NetworkPrefabsList.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkManager에 리스트 등록 & PlayerPrefab은 null로 (자동 스폰 방지)
|
||||||
|
var so = new SerializedObject(nm);
|
||||||
|
so.Update();
|
||||||
|
|
||||||
|
var playerPrefabProp = so.FindProperty("NetworkConfig.PlayerPrefab");
|
||||||
|
if (playerPrefabProp != null)
|
||||||
|
playerPrefabProp.objectReferenceValue = null;
|
||||||
|
|
||||||
|
var nmListsProp = so.FindProperty("NetworkConfig.Prefabs.NetworkPrefabsLists");
|
||||||
|
if (nmListsProp != null)
|
||||||
|
{
|
||||||
|
bool alreadyAdded = false;
|
||||||
|
for (int i = 0; i < nmListsProp.arraySize; i++)
|
||||||
|
{
|
||||||
|
if (nmListsProp.GetArrayElementAtIndex(i).objectReferenceValue == prefabList)
|
||||||
|
{ alreadyAdded = true; break; }
|
||||||
|
}
|
||||||
|
if (!alreadyAdded)
|
||||||
|
{
|
||||||
|
// null 항목 제거 후 추가
|
||||||
|
for (int i = nmListsProp.arraySize - 1; i >= 0; i--)
|
||||||
|
if (nmListsProp.GetArrayElementAtIndex(i).objectReferenceValue == null)
|
||||||
|
nmListsProp.DeleteArrayElementAtIndex(i);
|
||||||
|
|
||||||
|
int idx = nmListsProp.arraySize;
|
||||||
|
nmListsProp.InsertArrayElementAtIndex(idx);
|
||||||
|
nmListsProp.GetArrayElementAtIndex(idx).objectReferenceValue = prefabList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError("[LobbySceneBuilder] Could not find NetworkConfig.Prefabs.NetworkPrefabsLists property.");
|
||||||
|
}
|
||||||
|
|
||||||
|
so.ApplyModifiedProperties();
|
||||||
|
EditorSceneManager.MarkSceneDirty(nm.gameObject.scene);
|
||||||
|
EditorSceneManager.SaveScene(nm.gameObject.scene);
|
||||||
|
Debug.Log("[LobbySceneBuilder] NetworkPrefabsList registered. PlayerPrefab cleared. Scene saved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("Colosseum/Open Lobby Scene")]
|
||||||
|
public static void OpenLobbyScene()
|
||||||
|
{
|
||||||
|
if (EditorApplication.isPlaying)
|
||||||
|
EditorApplication.isPlaying = false;
|
||||||
|
EditorSceneManager.OpenScene("Assets/Scenes/Lobby.unity");
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("Colosseum/Play Lobby Scene")]
|
||||||
|
public static void PlayLobbyScene()
|
||||||
|
{
|
||||||
|
if (!EditorApplication.isPlaying)
|
||||||
|
{
|
||||||
|
EditorSceneManager.OpenScene("Assets/Scenes/Lobby.unity");
|
||||||
|
EditorApplication.isPlaying = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("Colosseum/Stop Play")]
|
||||||
|
public static void StopPlay()
|
||||||
|
{
|
||||||
|
EditorApplication.isPlaying = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 헬퍼 메서드 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private static GameObject CreatePanel(Transform parent, string name)
|
||||||
|
{
|
||||||
|
var go = new GameObject(name);
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
go.AddComponent<RectTransform>();
|
||||||
|
var img = go.AddComponent<Image>();
|
||||||
|
img.color = new Color(0, 0, 0, 0.6f);
|
||||||
|
return go;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameObject CreateInputField(Transform parent, string name, string placeholder, float w, float h)
|
||||||
|
{
|
||||||
|
var go = new GameObject(name);
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
var rect = go.AddComponent<RectTransform>();
|
||||||
|
rect.sizeDelta = new Vector2(w, h);
|
||||||
|
go.AddComponent<Image>().color = new Color(0.15f, 0.15f, 0.15f, 1f);
|
||||||
|
var input = go.AddComponent<TMP_InputField>();
|
||||||
|
|
||||||
|
var textArea = new GameObject("Text Area");
|
||||||
|
textArea.transform.SetParent(go.transform, false);
|
||||||
|
var taRect = textArea.AddComponent<RectTransform>();
|
||||||
|
taRect.anchorMin = Vector2.zero;
|
||||||
|
taRect.anchorMax = Vector2.one;
|
||||||
|
taRect.offsetMin = new Vector2(8, 2);
|
||||||
|
taRect.offsetMax = new Vector2(-8, -2);
|
||||||
|
textArea.AddComponent<RectMask2D>();
|
||||||
|
|
||||||
|
var phGO = new GameObject("Placeholder");
|
||||||
|
phGO.transform.SetParent(textArea.transform, false);
|
||||||
|
var phRect = phGO.AddComponent<RectTransform>();
|
||||||
|
phRect.anchorMin = Vector2.zero;
|
||||||
|
phRect.anchorMax = Vector2.one;
|
||||||
|
phRect.offsetMin = phRect.offsetMax = Vector2.zero;
|
||||||
|
var phTmp = phGO.AddComponent<TextMeshProUGUI>();
|
||||||
|
phTmp.text = placeholder;
|
||||||
|
phTmp.fontSize = 18;
|
||||||
|
phTmp.color = new Color(0.5f, 0.5f, 0.5f);
|
||||||
|
|
||||||
|
var txtGO = new GameObject("Text");
|
||||||
|
txtGO.transform.SetParent(textArea.transform, false);
|
||||||
|
var txtRect = txtGO.AddComponent<RectTransform>();
|
||||||
|
txtRect.anchorMin = Vector2.zero;
|
||||||
|
txtRect.anchorMax = Vector2.one;
|
||||||
|
txtRect.offsetMin = txtRect.offsetMax = Vector2.zero;
|
||||||
|
var txt = txtGO.AddComponent<TextMeshProUGUI>();
|
||||||
|
txt.fontSize = 18;
|
||||||
|
txt.color = Color.white;
|
||||||
|
|
||||||
|
input.textViewport = taRect;
|
||||||
|
input.placeholder = phTmp;
|
||||||
|
input.textComponent = txt;
|
||||||
|
|
||||||
|
return go;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameObject CreateButton(Transform parent, string name, string label, float w, float h)
|
||||||
|
{
|
||||||
|
var go = new GameObject(name);
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
var rect = go.AddComponent<RectTransform>();
|
||||||
|
rect.sizeDelta = new Vector2(w, h);
|
||||||
|
go.AddComponent<Image>().color = new Color(0.2f, 0.5f, 0.8f, 1f);
|
||||||
|
go.AddComponent<Button>();
|
||||||
|
|
||||||
|
var txtGO = new GameObject("Text");
|
||||||
|
txtGO.transform.SetParent(go.transform, false);
|
||||||
|
var txtRect = txtGO.AddComponent<RectTransform>();
|
||||||
|
txtRect.anchorMin = Vector2.zero;
|
||||||
|
txtRect.anchorMax = Vector2.one;
|
||||||
|
txtRect.offsetMin = txtRect.offsetMax = Vector2.zero;
|
||||||
|
var tmp = txtGO.AddComponent<TextMeshProUGUI>();
|
||||||
|
tmp.text = label;
|
||||||
|
tmp.fontSize = 20;
|
||||||
|
tmp.alignment = TextAlignmentOptions.Center;
|
||||||
|
tmp.color = Color.white;
|
||||||
|
|
||||||
|
return go;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextMeshProUGUI CreateLabel(Transform parent, string name, string text, int size)
|
||||||
|
{
|
||||||
|
var go = new GameObject(name);
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
var rect = go.AddComponent<RectTransform>();
|
||||||
|
rect.sizeDelta = new Vector2(400, size * 1.6f);
|
||||||
|
var tmp = go.AddComponent<TextMeshProUGUI>();
|
||||||
|
tmp.text = text;
|
||||||
|
tmp.fontSize = size;
|
||||||
|
tmp.alignment = TextAlignmentOptions.Center;
|
||||||
|
tmp.color = Color.white;
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetPrivateField(object obj, string fieldName, object value)
|
||||||
|
{
|
||||||
|
var field = obj.GetType().GetField(fieldName,
|
||||||
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
if (field != null)
|
||||||
|
field.SetValue(obj, value);
|
||||||
|
else
|
||||||
|
Debug.LogWarning($"[LobbySceneBuilder] Field not found: {fieldName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// NetworkPrefabHandler의 내부 List<NetworkPrefab>에 리플렉션으로 프리팹을 추가합니다.
|
||||||
|
/// (Prefabs 프로퍼티가 IReadOnlyList이므로 직접 Add 불가)
|
||||||
|
/// </summary>
|
||||||
|
private static void AddNetworkPrefab(NetworkManager nm, GameObject prefab)
|
||||||
|
{
|
||||||
|
var handler = nm.NetworkConfig.Prefabs;
|
||||||
|
var bindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
|
||||||
|
|
||||||
|
// 타입으로 직접 탐색 (필드명은 NGO 버전마다 다를 수 있음)
|
||||||
|
System.Collections.Generic.List<NetworkPrefab> list = null;
|
||||||
|
foreach (var field in handler.GetType().GetFields(bindingFlags))
|
||||||
|
{
|
||||||
|
if (field.FieldType == typeof(System.Collections.Generic.List<NetworkPrefab>))
|
||||||
|
{
|
||||||
|
list = field.GetValue(handler) as System.Collections.Generic.List<NetworkPrefab>;
|
||||||
|
if (list != null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[LobbySceneBuilder] Could not find List<NetworkPrefab> on NetworkPrefabHandler via reflection.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new NetworkPrefab { Prefab = prefab });
|
||||||
|
EditorUtility.SetDirty(nm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddSceneToBuildSettings(string scenePath, int index)
|
||||||
|
{
|
||||||
|
var scenes = EditorBuildSettings.scenes;
|
||||||
|
foreach (var s in scenes)
|
||||||
|
if (s.path == scenePath) return; // 이미 등록됨
|
||||||
|
|
||||||
|
var newScenes = new EditorBuildSettingsScene[scenes.Length + 1];
|
||||||
|
|
||||||
|
// index 위치에 삽입
|
||||||
|
int insertAt = Mathf.Clamp(index, 0, scenes.Length);
|
||||||
|
for (int i = 0; i < insertAt; i++)
|
||||||
|
newScenes[i] = scenes[i];
|
||||||
|
newScenes[insertAt] = new EditorBuildSettingsScene(scenePath, true);
|
||||||
|
for (int i = insertAt; i < scenes.Length; i++)
|
||||||
|
newScenes[i + 1] = scenes[i];
|
||||||
|
|
||||||
|
EditorBuildSettings.scenes = newScenes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Game/Scripts/Editor/LobbySceneBuilder.cs.meta
Normal file
2
Assets/_Game/Scripts/Editor/LobbySceneBuilder.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 84992c83a100d324c900f515f2f89734
|
||||||
147
Assets/_Game/Scripts/Network/LobbyManager.cs
Normal file
147
Assets/_Game/Scripts/Network/LobbyManager.cs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
using System;
|
||||||
|
using Unity.Collections;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using Unity.Netcode.Transports.UTP;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.SceneManagement;
|
||||||
|
|
||||||
|
namespace Colosseum.Network
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 로비 상태 관리. NetworkBehaviour로 플레이어 목록을 모든 클라이언트에 동기화.
|
||||||
|
/// NetworkManager가 있는 오브젝트와 별개로 배치하거나 NetworkObject 컴포넌트 필요.
|
||||||
|
/// </summary>
|
||||||
|
public class LobbyManager : NetworkBehaviour
|
||||||
|
{
|
||||||
|
public static LobbyManager Instance { get; private set; }
|
||||||
|
|
||||||
|
[Header("Game Scene")]
|
||||||
|
[SerializeField] private string gameSceneName = "Test";
|
||||||
|
|
||||||
|
// 모든 클라이언트에 동기화되는 플레이어 목록
|
||||||
|
private NetworkList<LobbyPlayerData> _players;
|
||||||
|
|
||||||
|
public event Action OnPlayersChanged;
|
||||||
|
|
||||||
|
// ─── 초기화 ───────────────────────────────────────────────
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null && Instance != this)
|
||||||
|
{
|
||||||
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
_players = new NetworkList<LobbyPlayerData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
_players.OnListChanged += _ => OnPlayersChanged?.Invoke();
|
||||||
|
|
||||||
|
if (IsServer)
|
||||||
|
{
|
||||||
|
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
|
||||||
|
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnected;
|
||||||
|
|
||||||
|
// 호스트 자신 추가
|
||||||
|
AddPlayer(NetworkManager.Singleton.LocalClientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkDespawn()
|
||||||
|
{
|
||||||
|
if (IsServer)
|
||||||
|
{
|
||||||
|
NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected;
|
||||||
|
NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 연결 제어 (클라이언트에서 호출) ─────────────────────
|
||||||
|
/// <summary>호스트 시작</summary>
|
||||||
|
public void StartHost(string ip, ushort port)
|
||||||
|
{
|
||||||
|
SetTransport(ip, port);
|
||||||
|
NetworkManager.Singleton.StartHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>클라이언트로 접속</summary>
|
||||||
|
public void StartClient(string ip, ushort port)
|
||||||
|
{
|
||||||
|
SetTransport(ip, port);
|
||||||
|
NetworkManager.Singleton.StartClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disconnect()
|
||||||
|
{
|
||||||
|
NetworkManager.Singleton?.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 플레이어 목록 ────────────────────────────────────────
|
||||||
|
public int PlayerCount => _players.Count;
|
||||||
|
|
||||||
|
public LobbyPlayerData GetPlayer(int index) => _players[index];
|
||||||
|
|
||||||
|
private void OnClientConnected(ulong clientId)
|
||||||
|
{
|
||||||
|
AddPlayer(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClientDisconnected(ulong clientId)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _players.Count; i++)
|
||||||
|
{
|
||||||
|
if (_players[i].ClientId == clientId)
|
||||||
|
{
|
||||||
|
_players.RemoveAt(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddPlayer(ulong clientId)
|
||||||
|
{
|
||||||
|
_players.Add(new LobbyPlayerData
|
||||||
|
{
|
||||||
|
ClientId = clientId,
|
||||||
|
PlayerName = new FixedString32Bytes($"Player {clientId}"),
|
||||||
|
IsReady = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 준비 상태 ────────────────────────────────────────────
|
||||||
|
[Rpc(SendTo.Server)]
|
||||||
|
public void SetReadyRpc(bool isReady, RpcParams rpcParams = default)
|
||||||
|
{
|
||||||
|
ulong clientId = rpcParams.Receive.SenderClientId;
|
||||||
|
for (int i = 0; i < _players.Count; i++)
|
||||||
|
{
|
||||||
|
if (_players[i].ClientId == clientId)
|
||||||
|
{
|
||||||
|
var data = _players[i];
|
||||||
|
data.IsReady = isReady;
|
||||||
|
_players[i] = data;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 게임 시작 ────────────────────────────────────────────
|
||||||
|
/// <summary>호스트만 호출 가능. 모든 플레이어가 준비되어야 활성화 권장.</summary>
|
||||||
|
public void StartGame()
|
||||||
|
{
|
||||||
|
if (!IsHost) return;
|
||||||
|
NetworkManager.Singleton.SceneManager.LoadScene(gameSceneName, LoadSceneMode.Single);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 유틸 ─────────────────────────────────────────────────
|
||||||
|
private void SetTransport(string ip, ushort port)
|
||||||
|
{
|
||||||
|
var transport = NetworkManager.Singleton?.GetComponent<UnityTransport>();
|
||||||
|
if (transport != null)
|
||||||
|
transport.SetConnectionData(ip, port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Game/Scripts/Network/LobbyManager.cs.meta
Normal file
2
Assets/_Game/Scripts/Network/LobbyManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8db7d99e81050fd4d8768e2fa31483a0
|
||||||
24
Assets/_Game/Scripts/Network/LobbyPlayerData.cs
Normal file
24
Assets/_Game/Scripts/Network/LobbyPlayerData.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Unity.Collections;
|
||||||
|
using Unity.Netcode;
|
||||||
|
|
||||||
|
namespace Colosseum.Network
|
||||||
|
{
|
||||||
|
public struct LobbyPlayerData : INetworkSerializable, System.IEquatable<LobbyPlayerData>
|
||||||
|
{
|
||||||
|
public ulong ClientId;
|
||||||
|
public FixedString32Bytes PlayerName;
|
||||||
|
public bool IsReady;
|
||||||
|
|
||||||
|
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
||||||
|
{
|
||||||
|
serializer.SerializeValue(ref ClientId);
|
||||||
|
serializer.SerializeValue(ref PlayerName);
|
||||||
|
serializer.SerializeValue(ref IsReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(LobbyPlayerData other) =>
|
||||||
|
ClientId == other.ClientId &&
|
||||||
|
PlayerName == other.PlayerName &&
|
||||||
|
IsReady == other.IsReady;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Game/Scripts/Network/LobbyPlayerData.cs.meta
Normal file
2
Assets/_Game/Scripts/Network/LobbyPlayerData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b07f5fad09b348b4387a5824c72f802f
|
||||||
@@ -53,6 +53,7 @@ namespace Colosseum.Player
|
|||||||
|
|
||||||
// 기존 메인 카메라 사용 또는 새로 생성
|
// 기존 메인 카메라 사용 또는 새로 생성
|
||||||
cameraInstance = Camera.main;
|
cameraInstance = Camera.main;
|
||||||
|
Debug.Log($"[PlayerCamera] Initialize: target={playerTransform?.name}, Camera.main={cameraInstance?.name ?? "NULL"}");
|
||||||
if (cameraInstance == null)
|
if (cameraInstance == null)
|
||||||
{
|
{
|
||||||
var cameraObject = new GameObject("PlayerCamera");
|
var cameraObject = new GameObject("PlayerCamera");
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ using Colosseum.Skills;
|
|||||||
namespace Colosseum.Player
|
namespace Colosseum.Player
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3인칭 플레이어 이동 (네트워크 동기화)
|
/// 서버 권한 이동.
|
||||||
|
/// - 오너(클라이언트/호스트): 입력 수집 → NetworkVariable에 월드 방향 기록
|
||||||
|
/// - 서버: NetworkVariable을 읽어 CharacterController 구동 → NetworkTransform으로 동기화
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RequireComponent(typeof(CharacterController))]
|
[RequireComponent(typeof(CharacterController))]
|
||||||
public class PlayerMovement : NetworkBehaviour
|
public class PlayerMovement : NetworkBehaviour
|
||||||
@@ -26,52 +28,68 @@ namespace Colosseum.Player
|
|||||||
|
|
||||||
private CharacterController characterController;
|
private CharacterController characterController;
|
||||||
private Vector3 velocity;
|
private Vector3 velocity;
|
||||||
private Vector2 moveInput;
|
private Vector2 moveInput; // 로컬 원시 입력 (IsOwner 전용)
|
||||||
private InputSystem_Actions inputActions;
|
private InputSystem_Actions inputActions;
|
||||||
private bool isJumping;
|
private bool isJumping;
|
||||||
private bool wasGrounded;
|
private bool wasGrounded;
|
||||||
|
|
||||||
|
// 클라이언트가 기록, 서버가 소비하는 월드 스페이스 이동 방향
|
||||||
|
private NetworkVariable<Vector2> netMoveInput = new NetworkVariable<Vector2>(
|
||||||
|
Vector2.zero,
|
||||||
|
NetworkVariableReadPermission.Everyone,
|
||||||
|
NetworkVariableWritePermission.Owner);
|
||||||
|
|
||||||
// 적 충돌 차단용
|
// 적 충돌 차단용
|
||||||
private Vector3 blockedDirection;
|
private Vector3 blockedDirection;
|
||||||
private readonly Collider[] overlapBuffer = new Collider[8];
|
private readonly Collider[] overlapBuffer = new Collider[8];
|
||||||
|
|
||||||
public float CurrentMoveSpeed => moveInput.magnitude * moveSpeed;
|
public float CurrentMoveSpeed => netMoveInput.Value.magnitude * moveSpeed;
|
||||||
public bool IsGrounded => characterController != null ? characterController.isGrounded : false;
|
public bool IsGrounded => characterController != null ? characterController.isGrounded : false;
|
||||||
public bool IsJumping => isJumping;
|
public bool IsJumping => isJumping;
|
||||||
|
|
||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
if (!IsOwner)
|
Debug.Log($"[PlayerMovement] LOCAL OnNetworkSpawn: OwnerClientId={OwnerClientId}, IsOwner={IsOwner}, IsServer={IsServer}, LocalClientId={NetworkManager.LocalClientId}");
|
||||||
|
ReportSpawnRpc(IsOwner, IsServer, IsLocalPlayer, NetworkManager.LocalClientId);
|
||||||
|
|
||||||
|
// 서버: 모든 플레이어의 이동 처리 담당
|
||||||
|
if (IsServer)
|
||||||
{
|
{
|
||||||
enabled = false;
|
characterController = GetComponent<CharacterController>();
|
||||||
return;
|
characterController.enableOverlapRecovery = false;
|
||||||
|
|
||||||
|
if (skillController == null)
|
||||||
|
skillController = GetComponent<SkillController>();
|
||||||
|
if (animator == null)
|
||||||
|
animator = GetComponentInChildren<Animator>();
|
||||||
|
|
||||||
|
SetSpawnPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
characterController = GetComponent<CharacterController>();
|
// 오너: 입력 및 카메라 초기화
|
||||||
// 보스 콜라이더가 겹칠 때 Unity 내부 자동 밀어냄 비활성화.
|
if (IsOwner)
|
||||||
// 적과의 분리는 EnemyBase.ResolvePlayerOverlap에서 보스 측이 담당.
|
{
|
||||||
characterController.enableOverlapRecovery = false;
|
InitializeInputActions();
|
||||||
|
SetupCamera();
|
||||||
if (skillController == null)
|
}
|
||||||
skillController = GetComponent<SkillController>();
|
|
||||||
|
|
||||||
if (animator == null)
|
|
||||||
animator = GetComponentInChildren<Animator>();
|
|
||||||
|
|
||||||
SetSpawnPosition();
|
|
||||||
InitializeInputActions();
|
|
||||||
SetupCamera();
|
|
||||||
|
|
||||||
|
// 서버도 오너도 아닌 클라이언트: 비활성화
|
||||||
|
if (!IsOwner && !IsServer)
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Rpc(SendTo.Server)]
|
||||||
|
private void ReportSpawnRpc(bool isOwner, bool isServer, bool isLocalPlayer, ulong localClientId)
|
||||||
|
{
|
||||||
|
Debug.Log($"[PlayerMovement] SPAWN REPORT: OwnerClientId={OwnerClientId}, IsOwner={isOwner}, IsServer={isServer}, IsLocalPlayer={isLocalPlayer}, LocalClientId={localClientId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeInputActions()
|
private void InitializeInputActions()
|
||||||
{
|
{
|
||||||
inputActions = new InputSystem_Actions();
|
inputActions = new InputSystem_Actions();
|
||||||
inputActions.Player.Enable();
|
inputActions.Player.Enable();
|
||||||
|
|
||||||
inputActions.Player.Move.performed += OnMovePerformed;
|
inputActions.Player.Move.performed += OnMovePerformed;
|
||||||
inputActions.Player.Move.canceled += OnMoveCanceled;
|
inputActions.Player.Move.canceled += OnMoveCanceled;
|
||||||
inputActions.Player.Jump.performed += OnJumpPerformed;
|
inputActions.Player.Jump.performed += OnJumpPerformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +98,7 @@ namespace Colosseum.Player
|
|||||||
if (inputActions != null)
|
if (inputActions != null)
|
||||||
{
|
{
|
||||||
inputActions.Player.Move.performed -= OnMovePerformed;
|
inputActions.Player.Move.performed -= OnMovePerformed;
|
||||||
inputActions.Player.Move.canceled -= OnMoveCanceled;
|
inputActions.Player.Move.canceled -= OnMoveCanceled;
|
||||||
inputActions.Player.Jump.performed -= OnJumpPerformed;
|
inputActions.Player.Jump.performed -= OnJumpPerformed;
|
||||||
inputActions.Player.Disable();
|
inputActions.Player.Disable();
|
||||||
}
|
}
|
||||||
@@ -98,7 +116,7 @@ namespace Colosseum.Player
|
|||||||
{
|
{
|
||||||
inputActions.Player.Enable();
|
inputActions.Player.Enable();
|
||||||
inputActions.Player.Move.performed += OnMovePerformed;
|
inputActions.Player.Move.performed += OnMovePerformed;
|
||||||
inputActions.Player.Move.canceled += OnMoveCanceled;
|
inputActions.Player.Move.canceled += OnMoveCanceled;
|
||||||
inputActions.Player.Jump.performed += OnJumpPerformed;
|
inputActions.Player.Jump.performed += OnJumpPerformed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,11 +139,21 @@ namespace Colosseum.Player
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void OnMovePerformed(InputAction.CallbackContext context) => moveInput = context.ReadValue<Vector2>();
|
private void OnMovePerformed(InputAction.CallbackContext context) => moveInput = context.ReadValue<Vector2>();
|
||||||
private void OnMoveCanceled(InputAction.CallbackContext context) => moveInput = Vector2.zero;
|
private void OnMoveCanceled(InputAction.CallbackContext context) => moveInput = Vector2.zero;
|
||||||
|
|
||||||
private void OnJumpPerformed(InputAction.CallbackContext context)
|
private void OnJumpPerformed(InputAction.CallbackContext context)
|
||||||
{
|
{
|
||||||
if (!isJumping && characterController.isGrounded)
|
if (!IsOwner) return;
|
||||||
|
JumpRequestRpc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 클라이언트가 점프 요청 → 서버가 검증 후 실행
|
||||||
|
/// </summary>
|
||||||
|
[Rpc(SendTo.Server)]
|
||||||
|
private void JumpRequestRpc()
|
||||||
|
{
|
||||||
|
if (!isJumping && characterController != null && characterController.isGrounded)
|
||||||
Jump();
|
Jump();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,26 +169,44 @@ namespace Colosseum.Player
|
|||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!IsOwner) return;
|
// 오너: 카메라 기준 이동 방향을 NetworkVariable에 기록
|
||||||
|
if (IsOwner)
|
||||||
|
UpdateNetworkInput();
|
||||||
|
|
||||||
ApplyGravity();
|
// 서버: NetworkVariable을 읽어 실제 이동 처리
|
||||||
UpdateBlockedDirection();
|
if (IsServer)
|
||||||
Move();
|
{
|
||||||
|
ApplyGravity();
|
||||||
|
UpdateBlockedDirection();
|
||||||
|
Move();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 매 프레임 주변 적을 능동적으로 감지하여 blockedDirection을 설정합니다.
|
/// 로컬 입력을 카메라 기준 월드 방향으로 변환해 NetworkVariable에 기록
|
||||||
/// 콜백 기반이 아니므로 보스가 플레이어 쪽으로 밀고 올 때도 즉시 감지합니다.
|
/// </summary>
|
||||||
|
private void UpdateNetworkInput()
|
||||||
|
{
|
||||||
|
Vector3 dir = new Vector3(moveInput.x, 0f, moveInput.y);
|
||||||
|
if (dir.sqrMagnitude > 0.001f)
|
||||||
|
dir = TransformDirectionByCamera(dir).normalized;
|
||||||
|
|
||||||
|
Vector2 worldDir = new Vector2(dir.x, dir.z);
|
||||||
|
if (netMoveInput.Value != worldDir)
|
||||||
|
netMoveInput.Value = worldDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 매 프레임 주변 적을 감지하여 blockedDirection 설정 (서버 전용)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void UpdateBlockedDirection()
|
private void UpdateBlockedDirection()
|
||||||
{
|
{
|
||||||
blockedDirection = Vector3.zero;
|
blockedDirection = Vector3.zero;
|
||||||
|
|
||||||
Vector3 center = transform.position + characterController.center;
|
Vector3 center = transform.position + characterController.center;
|
||||||
float radius = characterController.radius + 0.15f;
|
float radius = characterController.radius + 0.15f;
|
||||||
float halfHeight = Mathf.Max(0f, characterController.height * 0.5f - characterController.radius);
|
float halfHeight = Mathf.Max(0f, characterController.height * 0.5f - characterController.radius);
|
||||||
|
|
||||||
// 레이어 무관하게 NavMeshAgent 유무로 적 식별
|
|
||||||
int count = Physics.OverlapCapsuleNonAlloc(
|
int count = Physics.OverlapCapsuleNonAlloc(
|
||||||
center + Vector3.up * halfHeight,
|
center + Vector3.up * halfHeight,
|
||||||
center - Vector3.up * halfHeight,
|
center - Vector3.up * halfHeight,
|
||||||
@@ -193,7 +239,6 @@ namespace Colosseum.Player
|
|||||||
{
|
{
|
||||||
if (characterController == null) return;
|
if (characterController == null) return;
|
||||||
|
|
||||||
// 스킬 애니메이션 재생 중에는 이동 불가 (루트 모션은 OnAnimatorMove에서 처리)
|
|
||||||
if (skillController != null && skillController.IsPlayingAnimation)
|
if (skillController != null && skillController.IsPlayingAnimation)
|
||||||
{
|
{
|
||||||
if (!skillController.UsesRootMotion)
|
if (!skillController.UsesRootMotion)
|
||||||
@@ -201,11 +246,11 @@ namespace Colosseum.Player
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3 moveDirection = new Vector3(moveInput.x, 0f, moveInput.y);
|
// 클라이언트가 전송한 월드 스페이스 방향 사용
|
||||||
moveDirection = TransformDirectionByCamera(moveDirection);
|
Vector3 moveDirection = new Vector3(netMoveInput.Value.x, 0f, netMoveInput.Value.y);
|
||||||
moveDirection.Normalize();
|
if (moveDirection.sqrMagnitude > 0.001f)
|
||||||
|
moveDirection.Normalize();
|
||||||
|
|
||||||
// 적 방향으로 이동 시도 중이면 수평 이동 전체 취소
|
|
||||||
if (blockedDirection != Vector3.zero && Vector3.Dot(moveDirection, blockedDirection) > 0f)
|
if (blockedDirection != Vector3.zero && Vector3.Dot(moveDirection, blockedDirection) > 0f)
|
||||||
moveDirection = Vector3.zero;
|
moveDirection = Vector3.zero;
|
||||||
|
|
||||||
@@ -239,27 +284,25 @@ namespace Colosseum.Player
|
|||||||
{
|
{
|
||||||
if (Camera.main == null) return direction;
|
if (Camera.main == null) return direction;
|
||||||
|
|
||||||
Transform cam = Camera.main.transform;
|
Transform cam = Camera.main.transform;
|
||||||
Vector3 forward = new Vector3(cam.forward.x, 0f, cam.forward.z).normalized;
|
Vector3 forward = new Vector3(cam.forward.x, 0f, cam.forward.z).normalized;
|
||||||
Vector3 right = new Vector3(cam.right.x, 0f, cam.right.z).normalized;
|
Vector3 right = new Vector3(cam.right.x, 0f, cam.right.z).normalized;
|
||||||
|
|
||||||
return right * direction.x + forward * direction.z;
|
return right * direction.x + forward * direction.z;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 루트 모션 처리. 스킬 애니메이션 중 애니메이션의 이동/회전 데이터를 적용합니다.
|
/// 루트 모션 처리 (서버 전용 — NetworkTransform으로 동기화)
|
||||||
/// 적 방향으로의 이동은 취소합니다.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnAnimatorMove()
|
private void OnAnimatorMove()
|
||||||
{
|
{
|
||||||
if (!IsOwner) return;
|
if (!IsServer) return;
|
||||||
if (animator == null || characterController == null) return;
|
if (animator == null || characterController == null) return;
|
||||||
if (skillController == null || !skillController.IsPlayingAnimation) return;
|
if (skillController == null || !skillController.IsPlayingAnimation) return;
|
||||||
if (!skillController.UsesRootMotion) return;
|
if (!skillController.UsesRootMotion) return;
|
||||||
|
|
||||||
Vector3 deltaPosition = animator.deltaPosition;
|
Vector3 deltaPosition = animator.deltaPosition;
|
||||||
|
|
||||||
// 적 방향으로 루트 모션이 향하면 수평 이동 취소
|
|
||||||
if (blockedDirection != Vector3.zero)
|
if (blockedDirection != Vector3.zero)
|
||||||
{
|
{
|
||||||
Vector3 deltaXZ = new Vector3(deltaPosition.x, 0f, deltaPosition.z);
|
Vector3 deltaXZ = new Vector3(deltaPosition.x, 0f, deltaPosition.z);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Unity.Netcode;
|
||||||
|
|
||||||
namespace Colosseum.Skills
|
namespace Colosseum.Skills
|
||||||
{
|
{
|
||||||
@@ -7,7 +8,7 @@ namespace Colosseum.Skills
|
|||||||
/// 스킬 실행을 관리하는 컴포넌트.
|
/// 스킬 실행을 관리하는 컴포넌트.
|
||||||
/// 애니메이션 이벤트 기반으로 효과가 발동됩니다.
|
/// 애니메이션 이벤트 기반으로 효과가 발동됩니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SkillController : MonoBehaviour
|
public class SkillController : NetworkBehaviour
|
||||||
{
|
{
|
||||||
private const string SKILL_STATE_NAME = "Skill";
|
private const string SKILL_STATE_NAME = "Skill";
|
||||||
private const string END_STATE_NAME = "SkillEnd";
|
private const string END_STATE_NAME = "SkillEnd";
|
||||||
@@ -19,6 +20,10 @@ namespace Colosseum.Skills
|
|||||||
[Tooltip("Skill 상태에 연결된 기본 클립 (Override용)")]
|
[Tooltip("Skill 상태에 연결된 기본 클립 (Override용)")]
|
||||||
[SerializeField] private AnimationClip baseSkillClip;
|
[SerializeField] private AnimationClip baseSkillClip;
|
||||||
|
|
||||||
|
[Header("네트워크 동기화")]
|
||||||
|
[Tooltip("이 SkillController가 사용하는 모든 스킬/엔드 클립 (순서대로 인덱스 부여). 서버→클라이언트 클립 동기화에 사용됩니다.")]
|
||||||
|
[SerializeField] private List<AnimationClip> registeredClips = new();
|
||||||
|
|
||||||
[Header("설정")]
|
[Header("설정")]
|
||||||
[SerializeField] private bool debugMode = false;
|
[SerializeField] private bool debugMode = false;
|
||||||
[Tooltip("공격 범위 시각화 (Scene 뷰에서 확인)")]
|
[Tooltip("공격 범위 시각화 (Scene 뷰에서 확인)")]
|
||||||
@@ -34,6 +39,7 @@ namespace Colosseum.Skills
|
|||||||
// 쿨타임 추적
|
// 쿨타임 추적
|
||||||
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
|
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
|
||||||
|
|
||||||
|
|
||||||
public bool IsExecutingSkill => currentSkill != null && !skillEndRequested;
|
public bool IsExecutingSkill => currentSkill != null && !skillEndRequested;
|
||||||
public bool IsPlayingAnimation => currentSkill != null;
|
public bool IsPlayingAnimation => currentSkill != null;
|
||||||
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
|
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
|
||||||
@@ -55,6 +61,7 @@ namespace Colosseum.Skills
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (currentSkill == null || animator == null) return;
|
if (currentSkill == null || animator == null) return;
|
||||||
@@ -166,6 +173,10 @@ namespace Colosseum.Skills
|
|||||||
animator.Rebind();
|
animator.Rebind();
|
||||||
animator.Update(0f);
|
animator.Update(0f);
|
||||||
animator.Play(SKILL_STATE_NAME, 0, 0f);
|
animator.Play(SKILL_STATE_NAME, 0, 0f);
|
||||||
|
|
||||||
|
// 클라이언트에 클립 동기화
|
||||||
|
if (IsServer && IsSpawned)
|
||||||
|
PlaySkillClipClientRpc(registeredClips.IndexOf(clip));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -187,6 +198,10 @@ namespace Colosseum.Skills
|
|||||||
animator.Rebind();
|
animator.Rebind();
|
||||||
animator.Update(0f);
|
animator.Update(0f);
|
||||||
animator.Play(SKILL_STATE_NAME, 0, 0f);
|
animator.Play(SKILL_STATE_NAME, 0, 0f);
|
||||||
|
|
||||||
|
// 클라이언트에 클립 동기화
|
||||||
|
if (IsServer && IsSpawned)
|
||||||
|
PlaySkillClipClientRpc(registeredClips.IndexOf(clip));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -198,6 +213,41 @@ namespace Colosseum.Skills
|
|||||||
{
|
{
|
||||||
animator.runtimeAnimatorController = baseController;
|
animator.runtimeAnimatorController = baseController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 클라이언트에 복원 동기화
|
||||||
|
if (IsServer && IsSpawned)
|
||||||
|
RestoreBaseControllerClientRpc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 클라이언트: override 컨트롤러 적용 + 스킬 상태 재생 (원자적 실행으로 타이밍 문제 해결)
|
||||||
|
/// </summary>
|
||||||
|
[Rpc(SendTo.NotServer)]
|
||||||
|
private void PlaySkillClipClientRpc(int clipIndex)
|
||||||
|
{
|
||||||
|
if (baseSkillClip == null || animator == null || baseController == null) return;
|
||||||
|
if (clipIndex < 0 || clipIndex >= registeredClips.Count || registeredClips[clipIndex] == null)
|
||||||
|
{
|
||||||
|
if (debugMode) Debug.LogWarning($"[SkillController] Clip index {clipIndex} not found in registeredClips. Add it to sync to clients.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var overrideController = new AnimatorOverrideController(baseController);
|
||||||
|
overrideController[baseSkillClip] = registeredClips[clipIndex];
|
||||||
|
animator.runtimeAnimatorController = overrideController;
|
||||||
|
animator.Rebind();
|
||||||
|
animator.Update(0f);
|
||||||
|
animator.Play(SKILL_STATE_NAME, 0, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 클라이언트: 기본 컨트롤러 복원
|
||||||
|
/// </summary>
|
||||||
|
[Rpc(SendTo.NotServer)]
|
||||||
|
private void RestoreBaseControllerClientRpc()
|
||||||
|
{
|
||||||
|
if (animator != null && baseController != null)
|
||||||
|
animator.runtimeAnimatorController = baseController;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -206,6 +256,8 @@ namespace Colosseum.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void OnEffect(int index)
|
public void OnEffect(int index)
|
||||||
{
|
{
|
||||||
|
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return;
|
||||||
|
|
||||||
if (currentSkill == null)
|
if (currentSkill == null)
|
||||||
{
|
{
|
||||||
if (debugMode) Debug.LogWarning("[Effect] No skill executing");
|
if (debugMode) Debug.LogWarning("[Effect] No skill executing");
|
||||||
@@ -246,6 +298,8 @@ namespace Colosseum.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void OnSkillEnd()
|
public void OnSkillEnd()
|
||||||
{
|
{
|
||||||
|
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return;
|
||||||
|
|
||||||
if (currentSkill == null)
|
if (currentSkill == null)
|
||||||
{
|
{
|
||||||
if (debugMode) Debug.LogWarning("[SkillEnd] No skill executing");
|
if (debugMode) Debug.LogWarning("[SkillEnd] No skill executing");
|
||||||
|
|||||||
231
Assets/_Game/Scripts/UI/LobbyUI.cs
Normal file
231
Assets/_Game/Scripts/UI/LobbyUI.cs
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Colosseum.Network;
|
||||||
|
using TMPro;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace Colosseum.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 로비 씬 UI 제어.
|
||||||
|
/// 두 패널 구조:
|
||||||
|
/// connectPanel — IP/포트 입력 후 Host 또는 Join
|
||||||
|
/// lobbyPanel — 대기실 (플레이어 목록, 준비, 게임 시작)
|
||||||
|
/// </summary>
|
||||||
|
public class LobbyUI : MonoBehaviour
|
||||||
|
{
|
||||||
|
// ─── Connect Panel ────────────────────────────────────────
|
||||||
|
[Header("Connect Panel")]
|
||||||
|
[SerializeField] private GameObject connectPanel;
|
||||||
|
[SerializeField] private TMP_InputField ipInput;
|
||||||
|
[SerializeField] private TMP_InputField portInput;
|
||||||
|
[SerializeField] private Button hostButton;
|
||||||
|
[SerializeField] private Button joinButton;
|
||||||
|
[SerializeField] private TextMeshProUGUI connectStatusText;
|
||||||
|
|
||||||
|
// ─── Lobby Panel ──────────────────────────────────────────
|
||||||
|
[Header("Lobby Panel")]
|
||||||
|
[SerializeField] private GameObject lobbyPanel;
|
||||||
|
[SerializeField] private Transform playerListParent;
|
||||||
|
[SerializeField] private GameObject playerSlotPrefab; // TextMeshProUGUI 하나 포함
|
||||||
|
[SerializeField] private Button readyButton;
|
||||||
|
[SerializeField] private Button startButton; // 호스트만 표시
|
||||||
|
[SerializeField] private Button disconnectButton;
|
||||||
|
|
||||||
|
// ─── 내부 ─────────────────────────────────────────────────
|
||||||
|
private readonly List<GameObject> _slots = new();
|
||||||
|
private bool _isReady;
|
||||||
|
|
||||||
|
// ─── 초기화 ───────────────────────────────────────────────
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
ipInput.text = "127.0.0.1";
|
||||||
|
portInput.text = "7777";
|
||||||
|
|
||||||
|
hostButton.onClick.AddListener(OnHostClicked);
|
||||||
|
joinButton.onClick.AddListener(OnJoinClicked);
|
||||||
|
readyButton.onClick.AddListener(OnReadyClicked);
|
||||||
|
startButton.onClick.AddListener(OnStartClicked);
|
||||||
|
disconnectButton.onClick.AddListener(OnDisconnectClicked);
|
||||||
|
|
||||||
|
ShowConnectPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (NetworkManager.Singleton == null) return;
|
||||||
|
NetworkManager.Singleton.OnClientConnectedCallback += OnConnectionEvent;
|
||||||
|
NetworkManager.Singleton.OnClientDisconnectCallback += OnConnectionEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
if (NetworkManager.Singleton != null)
|
||||||
|
{
|
||||||
|
NetworkManager.Singleton.OnClientConnectedCallback -= OnConnectionEvent;
|
||||||
|
NetworkManager.Singleton.OnClientDisconnectCallback -= OnConnectionEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Connect Panel 버튼 ───────────────────────────────────
|
||||||
|
private void OnHostClicked()
|
||||||
|
{
|
||||||
|
if (!TryGetAddress(out string ip, out ushort port)) return;
|
||||||
|
LobbyManager.Instance.StartHost(ip, port);
|
||||||
|
LobbyManager.Instance.OnPlayersChanged += RefreshPlayerList;
|
||||||
|
ShowLobbyPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnJoinClicked()
|
||||||
|
{
|
||||||
|
if (!TryGetAddress(out string ip, out ushort port)) return;
|
||||||
|
SetConnectStatus("Connecting...");
|
||||||
|
joinButton.interactable = false;
|
||||||
|
hostButton.interactable = false;
|
||||||
|
|
||||||
|
LobbyManager.Instance.StartClient(ip, port);
|
||||||
|
|
||||||
|
// 연결 성공/실패 대기
|
||||||
|
NetworkManager.Singleton.OnClientConnectedCallback += OnJoinSuccess;
|
||||||
|
NetworkManager.Singleton.OnTransportFailure += OnJoinFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnJoinSuccess(ulong clientId)
|
||||||
|
{
|
||||||
|
if (clientId != NetworkManager.Singleton.LocalClientId) return;
|
||||||
|
|
||||||
|
NetworkManager.Singleton.OnClientConnectedCallback -= OnJoinSuccess;
|
||||||
|
NetworkManager.Singleton.OnTransportFailure -= OnJoinFailed;
|
||||||
|
|
||||||
|
LobbyManager.Instance.OnPlayersChanged += RefreshPlayerList;
|
||||||
|
ShowLobbyPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnJoinFailed()
|
||||||
|
{
|
||||||
|
NetworkManager.Singleton.OnClientConnectedCallback -= OnJoinSuccess;
|
||||||
|
NetworkManager.Singleton.OnTransportFailure -= OnJoinFailed;
|
||||||
|
|
||||||
|
SetConnectStatus("Connection failed. Check IP/Port.");
|
||||||
|
joinButton.interactable = true;
|
||||||
|
hostButton.interactable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Lobby Panel 버튼 ─────────────────────────────────────
|
||||||
|
private void OnReadyClicked()
|
||||||
|
{
|
||||||
|
_isReady = !_isReady;
|
||||||
|
LobbyManager.Instance.SetReadyRpc(_isReady);
|
||||||
|
readyButton.GetComponentInChildren<TextMeshProUGUI>().text =
|
||||||
|
_isReady ? "Ready!" : "Ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStartClicked()
|
||||||
|
{
|
||||||
|
LobbyManager.Instance.StartGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisconnectClicked()
|
||||||
|
{
|
||||||
|
LobbyManager.Instance.OnPlayersChanged -= RefreshPlayerList;
|
||||||
|
LobbyManager.Instance.Disconnect();
|
||||||
|
_isReady = false;
|
||||||
|
ShowConnectPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 패널 전환 ────────────────────────────────────────────
|
||||||
|
private void ShowConnectPanel()
|
||||||
|
{
|
||||||
|
connectPanel.SetActive(true);
|
||||||
|
lobbyPanel.SetActive(false);
|
||||||
|
SetConnectStatus("");
|
||||||
|
hostButton.interactable = true;
|
||||||
|
joinButton.interactable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowLobbyPanel()
|
||||||
|
{
|
||||||
|
connectPanel.SetActive(false);
|
||||||
|
lobbyPanel.SetActive(true);
|
||||||
|
|
||||||
|
bool isHost = NetworkManager.Singleton.IsHost;
|
||||||
|
startButton.gameObject.SetActive(isHost);
|
||||||
|
readyButton.gameObject.SetActive(!isHost);
|
||||||
|
|
||||||
|
RefreshPlayerList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 플레이어 목록 갱신 ───────────────────────────────────
|
||||||
|
private void OnConnectionEvent(ulong _) => RefreshPlayerList();
|
||||||
|
|
||||||
|
private void RefreshPlayerList()
|
||||||
|
{
|
||||||
|
if (LobbyManager.Instance == null) return;
|
||||||
|
|
||||||
|
// 슬롯 수 맞추기
|
||||||
|
int count = LobbyManager.Instance.PlayerCount;
|
||||||
|
while (_slots.Count < count)
|
||||||
|
{
|
||||||
|
var slot = Instantiate(playerSlotPrefab, playerListParent);
|
||||||
|
_slots.Add(slot);
|
||||||
|
}
|
||||||
|
while (_slots.Count > count)
|
||||||
|
{
|
||||||
|
Destroy(_slots[^1]);
|
||||||
|
_slots.RemoveAt(_slots.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var data = LobbyManager.Instance.GetPlayer(i);
|
||||||
|
var label = _slots[i].GetComponentInChildren<TextMeshProUGUI>();
|
||||||
|
if (label != null)
|
||||||
|
label.text = $"{data.PlayerName} {(data.IsReady ? "[Ready]" : "")}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 호스트: 모두 준비됐을 때만 시작 활성화
|
||||||
|
if (NetworkManager.Singleton.IsHost)
|
||||||
|
{
|
||||||
|
bool allReady = AllPlayersReady();
|
||||||
|
startButton.interactable = allReady && count >= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool AllPlayersReady()
|
||||||
|
{
|
||||||
|
if (LobbyManager.Instance == null) return false;
|
||||||
|
int count = LobbyManager.Instance.PlayerCount;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
// 호스트 본인은 준비 체크 면제
|
||||||
|
if (LobbyManager.Instance.GetPlayer(i).ClientId ==
|
||||||
|
NetworkManager.Singleton.LocalClientId) continue;
|
||||||
|
|
||||||
|
if (!LobbyManager.Instance.GetPlayer(i).IsReady) return false;
|
||||||
|
}
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 유틸 ─────────────────────────────────────────────────
|
||||||
|
private bool TryGetAddress(out string ip, out ushort port)
|
||||||
|
{
|
||||||
|
ip = ipInput.text.Trim();
|
||||||
|
if (string.IsNullOrEmpty(ip)) ip = "127.0.0.1";
|
||||||
|
|
||||||
|
if (!ushort.TryParse(portInput.text.Trim(), out port))
|
||||||
|
{
|
||||||
|
port = 7777;
|
||||||
|
SetConnectStatus("Invalid port number.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetConnectStatus(string msg)
|
||||||
|
{
|
||||||
|
if (connectStatusText != null)
|
||||||
|
connectStatusText.text = msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Game/Scripts/UI/LobbyUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/LobbyUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fa0ac7df4e465a4458e7d7dcc073d648
|
||||||
@@ -34,6 +34,10 @@ namespace Colosseum.Weapons
|
|||||||
[Tooltip("시작 무기 (선택)")]
|
[Tooltip("시작 무기 (선택)")]
|
||||||
[SerializeField] private WeaponData startingWeapon;
|
[SerializeField] private WeaponData startingWeapon;
|
||||||
|
|
||||||
|
[Header("네트워크 동기화")]
|
||||||
|
[Tooltip("이 장착 시스템이 사용하는 모든 WeaponData 목록. 서버→클라이언트 무기 동기화에 사용됩니다.")]
|
||||||
|
[SerializeField] private System.Collections.Generic.List<WeaponData> registeredWeapons = new();
|
||||||
|
|
||||||
// 캐싱된 소켓 Transform들
|
// 캐싱된 소켓 Transform들
|
||||||
private Transform rightHandSocket;
|
private Transform rightHandSocket;
|
||||||
private Transform leftHandSocket;
|
private Transform leftHandSocket;
|
||||||
@@ -51,7 +55,7 @@ namespace Colosseum.Weapons
|
|||||||
private readonly System.Collections.Generic.Dictionary<StatType, StatModifier> activeModifiers
|
private readonly System.Collections.Generic.Dictionary<StatType, StatModifier> activeModifiers
|
||||||
= new System.Collections.Generic.Dictionary<StatType, StatModifier>();
|
= new System.Collections.Generic.Dictionary<StatType, StatModifier>();
|
||||||
|
|
||||||
// 무기 장착 상태 동기화
|
// 무기 장착 상태 동기화 (registeredWeapons 인덱스, -1 = 없음)
|
||||||
private NetworkVariable<int> equippedWeaponId = new NetworkVariable<int>(-1);
|
private NetworkVariable<int> equippedWeaponId = new NetworkVariable<int>(-1);
|
||||||
|
|
||||||
public WeaponData CurrentWeapon => currentWeapon;
|
public WeaponData CurrentWeapon => currentWeapon;
|
||||||
@@ -81,14 +85,17 @@ namespace Colosseum.Weapons
|
|||||||
|
|
||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
// 네트워크 변수 변경 콜백
|
|
||||||
equippedWeaponId.OnValueChanged += HandleEquippedWeaponChanged;
|
equippedWeaponId.OnValueChanged += HandleEquippedWeaponChanged;
|
||||||
|
|
||||||
// 서버에서 시작 무기 장착
|
|
||||||
if (IsServer && startingWeapon != null)
|
if (IsServer && startingWeapon != null)
|
||||||
{
|
{
|
||||||
EquipWeapon(startingWeapon);
|
EquipWeapon(startingWeapon);
|
||||||
}
|
}
|
||||||
|
else if (!IsServer && equippedWeaponId.Value >= 0)
|
||||||
|
{
|
||||||
|
// 늦게 접속한 클라이언트: 현재 장착된 무기 시각화
|
||||||
|
SpawnWeaponVisualsLocal(equippedWeaponId.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNetworkDespawn()
|
public override void OnNetworkDespawn()
|
||||||
@@ -150,14 +157,12 @@ namespace Colosseum.Weapons
|
|||||||
|
|
||||||
private void HandleEquippedWeaponChanged(int oldValue, int newValue)
|
private void HandleEquippedWeaponChanged(int oldValue, int newValue)
|
||||||
{
|
{
|
||||||
// -1이면 무기 해제, 그 외에는 무기 장착됨
|
if (IsServer) return; // 서버는 EquipWeapon/UnequipWeapon에서 직접 처리
|
||||||
// (GetInstanceID()는 음수를 반환할 수 있으므로 >= 0 체크 사용 불가)
|
|
||||||
if (newValue == -1)
|
if (newValue == -1)
|
||||||
{
|
|
||||||
UnequipWeaponInternal();
|
UnequipWeaponInternal();
|
||||||
}
|
else
|
||||||
// 클라이언트에서는 서버에서 이미 장착된 무기 정보를 받아야 함
|
SpawnWeaponVisualsLocal(newValue);
|
||||||
// TODO: WeaponDatabase에서 ID로 WeaponData 조회
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -192,8 +197,10 @@ namespace Colosseum.Weapons
|
|||||||
// 무기 외형 생성 및 부착
|
// 무기 외형 생성 및 부착
|
||||||
SpawnWeaponVisuals(weapon);
|
SpawnWeaponVisuals(weapon);
|
||||||
|
|
||||||
// 네트워크 동기화 (간단한 ID 사용, 실제로는 WeaponDatabase 필요)
|
// registeredWeapons 인덱스로 동기화
|
||||||
equippedWeaponId.Value = weapon.GetInstanceID();
|
equippedWeaponId.Value = registeredWeapons.IndexOf(weapon);
|
||||||
|
if (equippedWeaponId.Value < 0)
|
||||||
|
Debug.LogWarning($"[WeaponEquipment] '{weapon.WeaponName}' is not in registeredWeapons. Add it to sync to clients.");
|
||||||
|
|
||||||
// 이벤트 발생
|
// 이벤트 발생
|
||||||
OnWeaponEquipped?.Invoke(weapon);
|
OnWeaponEquipped?.Invoke(weapon);
|
||||||
@@ -236,12 +243,10 @@ namespace Colosseum.Weapons
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void UnequipWeaponInternal()
|
private void UnequipWeaponInternal()
|
||||||
{
|
{
|
||||||
if (currentWeapon == null) return;
|
if (currentWeaponInstance == null && currentWeapon == null) return;
|
||||||
|
|
||||||
WeaponData previousWeapon = currentWeapon;
|
WeaponData previousWeapon = currentWeapon;
|
||||||
RemoveStatBonuses();
|
DespawnWeaponVisualsLocal();
|
||||||
DespawnWeaponVisuals();
|
|
||||||
currentWeapon = null;
|
|
||||||
|
|
||||||
OnWeaponUnequipped?.Invoke(previousWeapon);
|
OnWeaponUnequipped?.Invoke(previousWeapon);
|
||||||
}
|
}
|
||||||
@@ -294,13 +299,12 @@ namespace Colosseum.Weapons
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 무기 외형 생성 및 부착
|
/// 무기 외형 생성 및 부착 (서버)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void SpawnWeaponVisuals(WeaponData weapon)
|
private void SpawnWeaponVisuals(WeaponData weapon)
|
||||||
{
|
{
|
||||||
if (weapon == null || weapon.WeaponPrefab == null) return;
|
if (weapon == null || weapon.WeaponPrefab == null) return;
|
||||||
|
|
||||||
// 적절한 소켓 찾기
|
|
||||||
Transform socket = GetSocketForSlot(weapon.WeaponSlot);
|
Transform socket = GetSocketForSlot(weapon.WeaponSlot);
|
||||||
if (socket == null)
|
if (socket == null)
|
||||||
{
|
{
|
||||||
@@ -308,59 +312,63 @@ namespace Colosseum.Weapons
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 무기 인스턴스 생성
|
|
||||||
currentWeaponInstance = Instantiate(weapon.WeaponPrefab, socket);
|
|
||||||
currentWeaponInstance.transform.localPosition = weapon.PositionOffset;
|
|
||||||
currentWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset);
|
|
||||||
|
|
||||||
// 소켓 스케일 보정 (부모 스케일이 작은 경우 무기도 작아지는 문제 해결)
|
|
||||||
Vector3 scaleCompensation = new Vector3(
|
|
||||||
socket.lossyScale.x != 0 ? 1f / socket.lossyScale.x : 1f,
|
|
||||||
socket.lossyScale.y != 0 ? 1f / socket.lossyScale.y : 1f,
|
|
||||||
socket.lossyScale.z != 0 ? 1f / socket.lossyScale.z : 1f
|
|
||||||
);
|
|
||||||
currentWeaponInstance.transform.localScale = Vector3.Scale(weapon.Scale, scaleCompensation);
|
|
||||||
currentWeaponInstance = Instantiate(weapon.WeaponPrefab, socket);
|
currentWeaponInstance = Instantiate(weapon.WeaponPrefab, socket);
|
||||||
currentWeaponInstance.transform.localPosition = weapon.PositionOffset;
|
currentWeaponInstance.transform.localPosition = weapon.PositionOffset;
|
||||||
currentWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset);
|
currentWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset);
|
||||||
currentWeaponInstance.transform.localScale = weapon.Scale;
|
currentWeaponInstance.transform.localScale = weapon.Scale;
|
||||||
|
|
||||||
// 디버그: 스케일 정보 출력
|
|
||||||
Debug.Log($"[WeaponEquipment] Weapon instantiated - LocalScale: {currentWeaponInstance.transform.localScale}, LossyScale: {currentWeaponInstance.transform.lossyScale}");
|
|
||||||
Debug.Log($"[WeaponEquipment] Socket: {socket.name}, Socket scale: {socket.lossyScale}");
|
|
||||||
Debug.Log($"[WeaponEquipment] Position offset: {weapon.PositionOffset}, Rotation offset: {weapon.RotationOffset}");
|
|
||||||
|
|
||||||
// 네트워크 동기화를 위해 Spawn (서버에서만)
|
|
||||||
if (IsServer && currentWeaponInstance.TryGetComponent<NetworkObject>(out var networkObject))
|
|
||||||
{
|
|
||||||
networkObject.Spawn(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Log($"[WeaponEquipment] Spawned weapon visual: {weapon.WeaponName}");
|
Debug.Log($"[WeaponEquipment] Spawned weapon visual: {weapon.WeaponName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 무기 외형 제거
|
/// 클라이언트: registeredWeapons 인덱스로 무기 외형 생성
|
||||||
|
/// </summary>
|
||||||
|
private void SpawnWeaponVisualsLocal(int weaponIndex)
|
||||||
|
{
|
||||||
|
if (weaponIndex < 0 || weaponIndex >= registeredWeapons.Count || registeredWeapons[weaponIndex] == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[WeaponEquipment] Weapon index {weaponIndex} not found in registeredWeapons.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var weapon = registeredWeapons[weaponIndex];
|
||||||
|
if (weapon.WeaponPrefab == null) return;
|
||||||
|
|
||||||
|
DespawnWeaponVisualsLocal();
|
||||||
|
|
||||||
|
Transform socket = GetSocketForSlot(weapon.WeaponSlot);
|
||||||
|
if (socket == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[WeaponEquipment] No socket found for slot: {weapon.WeaponSlot}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWeaponInstance = Instantiate(weapon.WeaponPrefab, socket);
|
||||||
|
currentWeaponInstance.transform.localPosition = weapon.PositionOffset;
|
||||||
|
currentWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset);
|
||||||
|
currentWeaponInstance.transform.localScale = weapon.Scale;
|
||||||
|
currentWeapon = weapon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 무기 외형 제거 (서버)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void DespawnWeaponVisuals()
|
private void DespawnWeaponVisuals()
|
||||||
{
|
{
|
||||||
if (currentWeaponInstance == null) return;
|
if (currentWeaponInstance == null) return;
|
||||||
|
Destroy(currentWeaponInstance);
|
||||||
// 네트워크 Object면 Despawn
|
|
||||||
if (currentWeaponInstance.TryGetComponent<NetworkObject>(out var networkObject) && networkObject.IsSpawned)
|
|
||||||
{
|
|
||||||
if (IsServer)
|
|
||||||
{
|
|
||||||
networkObject.Despawn(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Destroy(currentWeaponInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentWeaponInstance = null;
|
currentWeaponInstance = null;
|
||||||
Debug.Log("[WeaponEquipment] Despawned weapon visual");
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 무기 외형 제거 (클라이언트)
|
||||||
|
/// </summary>
|
||||||
|
private void DespawnWeaponVisualsLocal()
|
||||||
|
{
|
||||||
|
if (currentWeaponInstance == null) return;
|
||||||
|
Destroy(currentWeaponInstance);
|
||||||
|
currentWeaponInstance = null;
|
||||||
|
currentWeapon = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ EditorBuildSettings:
|
|||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Scenes:
|
m_Scenes:
|
||||||
|
- enabled: 1
|
||||||
|
path: Assets/Scenes/Lobby.unity
|
||||||
|
guid: 1999405fa34dae341b5f919ead00585b
|
||||||
- enabled: 1
|
- enabled: 1
|
||||||
path: Assets/Scenes/SampleScene.unity
|
path: Assets/Scenes/SampleScene.unity
|
||||||
guid: 99c9720ab356a0642a771bea13969a05
|
guid: 99c9720ab356a0642a771bea13969a05
|
||||||
|
|||||||
Reference in New Issue
Block a user