Add player action state and abnormality debug workflow
This commit is contained in:
@@ -322,6 +322,11 @@ public class NetworkedComponent : NetworkBehaviour
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
- For Unity work, prefer Unity MCP for active scene inspection, runtime verification, prefab checks, and console review when it is available in the session.
|
||||||
|
- Never edit code, scenes, prefabs, components, or Unity asset settings while the Unity Editor is in play mode. Stop play mode first, then edit.
|
||||||
|
- After Unity-related edits, refresh or compile as needed and check the Unity console before proceeding.
|
||||||
|
- For networked play tests, prefer a temporary non-conflicting test port when needed and restore the default port after validation.
|
||||||
|
- The user has a strong project preference that play mode must be stopped before edits because network ports can remain occupied otherwise.
|
||||||
- All code comments and documentation should be in Korean
|
- All code comments and documentation should be in Korean
|
||||||
- Use `[Min()]` attribute for numeric minimums in Inspector
|
- Use `[Min()]` attribute for numeric minimums in Inspector
|
||||||
- Use `[TextArea]` for multi-line string fields
|
- Use `[TextArea]` for multi-line string fields
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
%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: b08cc671f858a3b409170a5356e960a0, type: 3}
|
||||||
|
m_Name: Data_Abnormality_Player_Silence
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Abnormalities.AbnormalityData
|
||||||
|
abnormalityName: 침묵
|
||||||
|
icon: {fileID: 0}
|
||||||
|
duration: 3
|
||||||
|
level: 1
|
||||||
|
isDebuff: 1
|
||||||
|
statModifiers: []
|
||||||
|
periodicInterval: 0
|
||||||
|
periodicValue: 0
|
||||||
|
controlType: 2
|
||||||
|
slowMultiplier: 0.5
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e365be692bf794d47af14aecd996fcb6
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
%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: b08cc671f858a3b409170a5356e960a0, type: 3}
|
||||||
|
m_Name: Data_Abnormality_Player_Stun
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Abnormalities.AbnormalityData
|
||||||
|
abnormalityName: 기절
|
||||||
|
icon: {fileID: 0}
|
||||||
|
duration: 2
|
||||||
|
level: 1
|
||||||
|
isDebuff: 1
|
||||||
|
statModifiers: []
|
||||||
|
periodicInterval: 0
|
||||||
|
periodicValue: 0
|
||||||
|
controlType: 1
|
||||||
|
slowMultiplier: 0.5
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6fed5160fb0f9444383fdd656ddc38cb
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
%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: bf750718c64c4bd48af905d2927351de, type: 3}
|
||||||
|
m_Name: "Data_SkillEffect_Player_\uAE30\uC808"
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.AbnormalityEffect
|
||||||
|
targetType: 1
|
||||||
|
targetTeam: 0
|
||||||
|
areaCenter: 0
|
||||||
|
areaShape: 0
|
||||||
|
targetLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
areaRadius: 2
|
||||||
|
fanOriginDistance: 1
|
||||||
|
fanRadius: 3
|
||||||
|
fanHalfAngle: 45
|
||||||
|
abnormalityData: {fileID: 11400000, guid: 6fed5160fb0f9444383fdd656ddc38cb, type: 2}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 57c4f5cb3bcd7ab4bad0520d024ae01f
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
%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: bf750718c64c4bd48af905d2927351de, type: 3}
|
||||||
|
m_Name: "Data_SkillEffect_Player_\uCE68\uBB35"
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.AbnormalityEffect
|
||||||
|
targetType: 1
|
||||||
|
targetTeam: 0
|
||||||
|
areaCenter: 0
|
||||||
|
areaShape: 0
|
||||||
|
targetLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
areaRadius: 2
|
||||||
|
fanOriginDistance: 1
|
||||||
|
fanRadius: 3
|
||||||
|
fanHalfAngle: 45
|
||||||
|
abnormalityData: {fileID: 11400000, guid: e365be692bf794d47af14aecd996fcb6, type: 2}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c5242fddf0b86774f9810ec0e6a8ca03
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -24,6 +24,9 @@ GameObject:
|
|||||||
- component: {fileID: 3552488436187204500}
|
- component: {fileID: 3552488436187204500}
|
||||||
- component: {fileID: -5132198055668300151}
|
- component: {fileID: -5132198055668300151}
|
||||||
- component: {fileID: -6410357568507457303}
|
- component: {fileID: -6410357568507457303}
|
||||||
|
- component: {fileID: 3574789915074274759}
|
||||||
|
- component: {fileID: 2540460367028266762}
|
||||||
|
- component: {fileID: 1829782337872253002}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: Prefab_Player_Default
|
m_Name: Prefab_Player_Default
|
||||||
m_TagString: Player
|
m_TagString: Player
|
||||||
@@ -139,6 +142,7 @@ MonoBehaviour:
|
|||||||
jumpForce: 5
|
jumpForce: 5
|
||||||
skillController: {fileID: 6912018896034183004}
|
skillController: {fileID: 6912018896034183004}
|
||||||
animator: {fileID: 3426985706796420257}
|
animator: {fileID: 3426985706796420257}
|
||||||
|
actionState: {fileID: 0}
|
||||||
--- !u!114 &194806265065691022
|
--- !u!114 &194806265065691022
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -324,6 +328,7 @@ MonoBehaviour:
|
|||||||
skillController: {fileID: 6912018896034183004}
|
skillController: {fileID: 6912018896034183004}
|
||||||
networkController: {fileID: 0}
|
networkController: {fileID: 0}
|
||||||
weaponEquipment: {fileID: 0}
|
weaponEquipment: {fileID: 0}
|
||||||
|
actionState: {fileID: 0}
|
||||||
--- !u!114 &1242716222252539497
|
--- !u!114 &1242716222252539497
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -398,6 +403,65 @@ MonoBehaviour:
|
|||||||
startingWeapon: {fileID: 11400000, guid: 646964ccbda84e947b97537d7f7813aa, type: 2}
|
startingWeapon: {fileID: 11400000, guid: 646964ccbda84e947b97537d7f7813aa, type: 2}
|
||||||
registeredWeapons:
|
registeredWeapons:
|
||||||
- {fileID: 11400000, guid: 646964ccbda84e947b97537d7f7813aa, type: 2}
|
- {fileID: 11400000, guid: 646964ccbda84e947b97537d7f7813aa, type: 2}
|
||||||
|
--- !u!114 &3574789915074274759
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 6473031571298860035}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 01f7f1d2e296d2046b2977e94b7269fe, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Player.PlayerActionState
|
||||||
|
networkController: {fileID: 0}
|
||||||
|
abnormalityManager: {fileID: 0}
|
||||||
|
skillController: {fileID: 0}
|
||||||
|
spectator: {fileID: 0}
|
||||||
|
--- !u!114 &2540460367028266762
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 6473031571298860035}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: bea222c7cf052d949984b6c08b08e545, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Player.PlayerAbnormalityDebugHUD
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
abnormalityManager: {fileID: 0}
|
||||||
|
networkController: {fileID: 0}
|
||||||
|
showOnStart: 0
|
||||||
|
debugLogs: 1
|
||||||
|
abnormalityCatalog: []
|
||||||
|
--- !u!114 &1829782337872253002
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 6473031571298860035}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 43d5dfd0218bf3445b8678dae42350d6, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Player.PlayerAbnormalityVerificationRunner
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
abnormalityManager: {fileID: 0}
|
||||||
|
actionState: {fileID: 0}
|
||||||
|
networkController: {fileID: 0}
|
||||||
|
stunData: {fileID: 0}
|
||||||
|
silenceData: {fileID: 0}
|
||||||
|
runOnStartInEditor: 0
|
||||||
|
settleDelay: 0.2
|
||||||
|
isRunning: 0
|
||||||
|
lastRunPassed: 0
|
||||||
|
totalChecks: 0
|
||||||
|
failedChecks: 0
|
||||||
|
lastReport:
|
||||||
--- !u!1001 &361239394574523229
|
--- !u!1001 &361239394574523229
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
@@ -28,23 +28,28 @@ namespace Colosseum.Abnormalities
|
|||||||
private int silenceCount;
|
private int silenceCount;
|
||||||
private float slowMultiplier = 1f;
|
private float slowMultiplier = 1f;
|
||||||
|
|
||||||
|
// 클라이언트 판정용 제어 효과 동기화 변수
|
||||||
|
private NetworkVariable<int> syncedStunCount = new NetworkVariable<int>(0);
|
||||||
|
private NetworkVariable<int> syncedSilenceCount = new NetworkVariable<int>(0);
|
||||||
|
private NetworkVariable<float> syncedSlowMultiplier = new NetworkVariable<float>(1f);
|
||||||
|
|
||||||
// 네트워크 동기화용 데이터
|
// 네트워크 동기화용 데이터
|
||||||
private NetworkList<AbnormalitySyncData> syncedAbnormalities;
|
private NetworkList<AbnormalitySyncData> syncedAbnormalities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 기절 상태 여부
|
/// 기절 상태 여부
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsStunned => stunCount > 0;
|
public bool IsStunned => GetCurrentStunCount() > 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 침묵 상태 여부
|
/// 침묵 상태 여부
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsSilenced => silenceCount > 0;
|
public bool IsSilenced => GetCurrentSilenceCount() > 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 이동 속도 배율 (1.0 = 기본, 0.5 = 50% 감소)
|
/// 이동 속도 배율 (1.0 = 기본, 0.5 = 50% 감소)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float MoveSpeedMultiplier => slowMultiplier;
|
public float MoveSpeedMultiplier => GetCurrentSlowMultiplier();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 행동 가능 여부 (기절이 아닐 때)
|
/// 행동 가능 여부 (기절이 아닐 때)
|
||||||
@@ -102,11 +107,33 @@ namespace Colosseum.Abnormalities
|
|||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
syncedAbnormalities.OnListChanged += OnSyncedAbnormalitiesChanged;
|
syncedAbnormalities.OnListChanged += OnSyncedAbnormalitiesChanged;
|
||||||
|
|
||||||
|
syncedStunCount.OnValueChanged += HandleSyncedStunChanged;
|
||||||
|
syncedSilenceCount.OnValueChanged += HandleSyncedSilenceChanged;
|
||||||
|
syncedSlowMultiplier.OnValueChanged += HandleSyncedSlowChanged;
|
||||||
|
|
||||||
|
if (networkController != null)
|
||||||
|
{
|
||||||
|
networkController.OnDeathStateChanged += HandleDeathStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsServer)
|
||||||
|
{
|
||||||
|
SyncControlEffects();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNetworkDespawn()
|
public override void OnNetworkDespawn()
|
||||||
{
|
{
|
||||||
syncedAbnormalities.OnListChanged -= OnSyncedAbnormalitiesChanged;
|
syncedAbnormalities.OnListChanged -= OnSyncedAbnormalitiesChanged;
|
||||||
|
syncedStunCount.OnValueChanged -= HandleSyncedStunChanged;
|
||||||
|
syncedSilenceCount.OnValueChanged -= HandleSyncedSilenceChanged;
|
||||||
|
syncedSlowMultiplier.OnValueChanged -= HandleSyncedSlowChanged;
|
||||||
|
|
||||||
|
if (networkController != null)
|
||||||
|
{
|
||||||
|
networkController.OnDeathStateChanged -= HandleDeathStateChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
@@ -129,6 +156,12 @@ namespace Colosseum.Abnormalities
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (networkController != null && networkController.IsDead)
|
||||||
|
{
|
||||||
|
Debug.Log($"[Abnormality] Ignored {data.abnormalityName} because {gameObject.name} is dead");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (IsServer)
|
if (IsServer)
|
||||||
{
|
{
|
||||||
ApplyAbnormalityInternal(data, source);
|
ApplyAbnormalityInternal(data, source);
|
||||||
@@ -386,6 +419,8 @@ namespace Colosseum.Abnormalities
|
|||||||
slowMultiplier = Mathf.Min(slowMultiplier, data.slowMultiplier);
|
slowMultiplier = Mathf.Min(slowMultiplier, data.slowMultiplier);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SyncControlEffects();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveControlEffect(AbnormalityData data)
|
private void RemoveControlEffect(AbnormalityData data)
|
||||||
@@ -404,6 +439,8 @@ namespace Colosseum.Abnormalities
|
|||||||
RecalculateSlowMultiplier();
|
RecalculateSlowMultiplier();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SyncControlEffects();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecalculateSlowMultiplier()
|
private void RecalculateSlowMultiplier()
|
||||||
@@ -419,6 +456,22 @@ namespace Colosseum.Abnormalities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int GetCurrentStunCount() => IsServer ? stunCount : syncedStunCount.Value;
|
||||||
|
|
||||||
|
private int GetCurrentSilenceCount() => IsServer ? silenceCount : syncedSilenceCount.Value;
|
||||||
|
|
||||||
|
private float GetCurrentSlowMultiplier() => IsServer ? slowMultiplier : syncedSlowMultiplier.Value;
|
||||||
|
|
||||||
|
private void SyncControlEffects()
|
||||||
|
{
|
||||||
|
if (!IsServer)
|
||||||
|
return;
|
||||||
|
|
||||||
|
syncedStunCount.Value = stunCount;
|
||||||
|
syncedSilenceCount.Value = silenceCount;
|
||||||
|
syncedSlowMultiplier.Value = slowMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
private void SyncAbnormalityAdd(ActiveAbnormality abnormality, GameObject source)
|
private void SyncAbnormalityAdd(ActiveAbnormality abnormality, GameObject source)
|
||||||
{
|
{
|
||||||
var sourceClientId = source != null && source.TryGetComponent<NetworkObject>(out var netObj) ? netObj.OwnerClientId : 0UL;
|
var sourceClientId = source != null && source.TryGetComponent<NetworkObject>(out var netObj) ? netObj.OwnerClientId : 0UL;
|
||||||
@@ -464,6 +517,45 @@ namespace Colosseum.Abnormalities
|
|||||||
OnAbnormalitiesChanged?.Invoke();
|
OnAbnormalitiesChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleSyncedStunChanged(int oldValue, int newValue)
|
||||||
|
{
|
||||||
|
if (oldValue == newValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
OnAbnormalitiesChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleSyncedSilenceChanged(int oldValue, int newValue)
|
||||||
|
{
|
||||||
|
if (oldValue == newValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
OnAbnormalitiesChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleSyncedSlowChanged(float oldValue, float newValue)
|
||||||
|
{
|
||||||
|
if (Mathf.Approximately(oldValue, newValue))
|
||||||
|
return;
|
||||||
|
|
||||||
|
OnAbnormalitiesChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 사망 시 활성 이상 상태를 모두 제거합니다.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleDeathStateChanged(bool dead)
|
||||||
|
{
|
||||||
|
if (!dead || !IsServer)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (activeAbnormalities.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RemoveAllAbnormalities();
|
||||||
|
Debug.Log($"[Abnormality] Cleared all abnormalities on death: {gameObject.name}");
|
||||||
|
}
|
||||||
|
|
||||||
private AbnormalityData FindAbnormalityDataById(int instanceId)
|
private AbnormalityData FindAbnormalityDataById(int instanceId)
|
||||||
{
|
{
|
||||||
var allData = Resources.FindObjectsOfTypeAll<AbnormalityData>();
|
var allData = Resources.FindObjectsOfTypeAll<AbnormalityData>();
|
||||||
|
|||||||
@@ -217,6 +217,15 @@ public partial class @InputSystem_Actions: IInputActionCollection2, IDisposable
|
|||||||
""processors"": """",
|
""processors"": """",
|
||||||
""interactions"": """",
|
""interactions"": """",
|
||||||
""initialStateCheck"": false
|
""initialStateCheck"": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": ""DebugHUD"",
|
||||||
|
""type"": ""Button"",
|
||||||
|
""id"": ""ae37625e-86d3-4579-8129-64ea49cf7e78"",
|
||||||
|
""expectedControlType"": """",
|
||||||
|
""processors"": """",
|
||||||
|
""interactions"": """",
|
||||||
|
""initialStateCheck"": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
""bindings"": [
|
""bindings"": [
|
||||||
@@ -593,6 +602,17 @@ public partial class @InputSystem_Actions: IInputActionCollection2, IDisposable
|
|||||||
""action"": ""Evade"",
|
""action"": ""Evade"",
|
||||||
""isComposite"": false,
|
""isComposite"": false,
|
||||||
""isPartOfComposite"": false
|
""isPartOfComposite"": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": """",
|
||||||
|
""id"": ""bb3c1259-8e98-4602-82f0-3fc4e8d74f0e"",
|
||||||
|
""path"": ""<Keyboard>/backquote"",
|
||||||
|
""interactions"": """",
|
||||||
|
""processors"": """",
|
||||||
|
""groups"": """",
|
||||||
|
""action"": ""DebugHUD"",
|
||||||
|
""isComposite"": false,
|
||||||
|
""isPartOfComposite"": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1192,6 +1212,7 @@ public partial class @InputSystem_Actions: IInputActionCollection2, IDisposable
|
|||||||
m_Player_Skill5 = m_Player.FindAction("Skill 5", throwIfNotFound: true);
|
m_Player_Skill5 = m_Player.FindAction("Skill 5", throwIfNotFound: true);
|
||||||
m_Player_Skill6 = m_Player.FindAction("Skill 6", throwIfNotFound: true);
|
m_Player_Skill6 = m_Player.FindAction("Skill 6", throwIfNotFound: true);
|
||||||
m_Player_Evade = m_Player.FindAction("Evade", throwIfNotFound: true);
|
m_Player_Evade = m_Player.FindAction("Evade", throwIfNotFound: true);
|
||||||
|
m_Player_DebugHUD = m_Player.FindAction("DebugHUD", throwIfNotFound: true);
|
||||||
// UI
|
// UI
|
||||||
m_UI = asset.FindActionMap("UI", throwIfNotFound: true);
|
m_UI = asset.FindActionMap("UI", throwIfNotFound: true);
|
||||||
m_UI_Navigate = m_UI.FindAction("Navigate", throwIfNotFound: true);
|
m_UI_Navigate = m_UI.FindAction("Navigate", throwIfNotFound: true);
|
||||||
@@ -1299,6 +1320,7 @@ public partial class @InputSystem_Actions: IInputActionCollection2, IDisposable
|
|||||||
private readonly InputAction m_Player_Skill5;
|
private readonly InputAction m_Player_Skill5;
|
||||||
private readonly InputAction m_Player_Skill6;
|
private readonly InputAction m_Player_Skill6;
|
||||||
private readonly InputAction m_Player_Evade;
|
private readonly InputAction m_Player_Evade;
|
||||||
|
private readonly InputAction m_Player_DebugHUD;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides access to input actions defined in input action map "Player".
|
/// Provides access to input actions defined in input action map "Player".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1367,6 +1389,10 @@ public partial class @InputSystem_Actions: IInputActionCollection2, IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public InputAction @Evade => m_Wrapper.m_Player_Evade;
|
public InputAction @Evade => m_Wrapper.m_Player_Evade;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Provides access to the underlying input action "Player/DebugHUD".
|
||||||
|
/// </summary>
|
||||||
|
public InputAction @DebugHUD => m_Wrapper.m_Player_DebugHUD;
|
||||||
|
/// <summary>
|
||||||
/// Provides access to the underlying input action map instance.
|
/// Provides access to the underlying input action map instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public InputActionMap Get() { return m_Wrapper.m_Player; }
|
public InputActionMap Get() { return m_Wrapper.m_Player; }
|
||||||
@@ -1434,6 +1460,9 @@ public partial class @InputSystem_Actions: IInputActionCollection2, IDisposable
|
|||||||
@Evade.started += instance.OnEvade;
|
@Evade.started += instance.OnEvade;
|
||||||
@Evade.performed += instance.OnEvade;
|
@Evade.performed += instance.OnEvade;
|
||||||
@Evade.canceled += instance.OnEvade;
|
@Evade.canceled += instance.OnEvade;
|
||||||
|
@DebugHUD.started += instance.OnDebugHUD;
|
||||||
|
@DebugHUD.performed += instance.OnDebugHUD;
|
||||||
|
@DebugHUD.canceled += instance.OnDebugHUD;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1487,6 +1516,9 @@ public partial class @InputSystem_Actions: IInputActionCollection2, IDisposable
|
|||||||
@Evade.started -= instance.OnEvade;
|
@Evade.started -= instance.OnEvade;
|
||||||
@Evade.performed -= instance.OnEvade;
|
@Evade.performed -= instance.OnEvade;
|
||||||
@Evade.canceled -= instance.OnEvade;
|
@Evade.canceled -= instance.OnEvade;
|
||||||
|
@DebugHUD.started -= instance.OnDebugHUD;
|
||||||
|
@DebugHUD.performed -= instance.OnDebugHUD;
|
||||||
|
@DebugHUD.canceled -= instance.OnDebugHUD;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1885,6 +1917,13 @@ public partial class @InputSystem_Actions: IInputActionCollection2, IDisposable
|
|||||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||||
void OnEvade(InputAction.CallbackContext context);
|
void OnEvade(InputAction.CallbackContext context);
|
||||||
|
/// <summary>
|
||||||
|
/// Method invoked when associated input action "DebugHUD" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||||
|
void OnDebugHUD(InputAction.CallbackContext context);
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface to implement callback methods for all input action callbacks associated with input actions defined by "UI" which allows adding and removing callbacks.
|
/// Interface to implement callback methods for all input action callbacks associated with input actions defined by "UI" which allows adding and removing callbacks.
|
||||||
|
|||||||
@@ -131,6 +131,15 @@
|
|||||||
"processors": "",
|
"processors": "",
|
||||||
"interactions": "",
|
"interactions": "",
|
||||||
"initialStateCheck": false
|
"initialStateCheck": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DebugHUD",
|
||||||
|
"type": "Button",
|
||||||
|
"id": "ae37625e-86d3-4579-8129-64ea49cf7e78",
|
||||||
|
"expectedControlType": "",
|
||||||
|
"processors": "",
|
||||||
|
"interactions": "",
|
||||||
|
"initialStateCheck": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"bindings": [
|
"bindings": [
|
||||||
@@ -507,6 +516,17 @@
|
|||||||
"action": "Evade",
|
"action": "Evade",
|
||||||
"isComposite": false,
|
"isComposite": false,
|
||||||
"isPartOfComposite": false
|
"isPartOfComposite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"id": "bb3c1259-8e98-4602-82f0-3fc4e8d74f0e",
|
||||||
|
"path": "<Keyboard>/backquote",
|
||||||
|
"interactions": "",
|
||||||
|
"processors": "",
|
||||||
|
"groups": "",
|
||||||
|
"action": "DebugHUD",
|
||||||
|
"isComposite": false,
|
||||||
|
"isPartOfComposite": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
442
Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs
Normal file
442
Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
|
||||||
|
using Unity.Netcode;
|
||||||
|
|
||||||
|
using Colosseum.Abnormalities;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Colosseum.Player
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 로컬 플레이어가 자신에게 이상상태를 적용/해제할 수 있는 디버그 HUD.
|
||||||
|
/// 이상상태 이름, 에셋 이름, 인덱스로 검색해 적용할 수 있습니다.
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class PlayerAbnormalityDebugHUD : NetworkBehaviour
|
||||||
|
{
|
||||||
|
[Header("References")]
|
||||||
|
[Tooltip("이상상태 관리자")]
|
||||||
|
[SerializeField] private AbnormalityManager abnormalityManager;
|
||||||
|
|
||||||
|
[Tooltip("플레이어 네트워크 상태")]
|
||||||
|
[SerializeField] private PlayerNetworkController networkController;
|
||||||
|
|
||||||
|
[Header("Display")]
|
||||||
|
[Tooltip("시작 시 HUD 표시 여부")]
|
||||||
|
[SerializeField] private bool showOnStart = false;
|
||||||
|
|
||||||
|
[Tooltip("로그 출력 여부")]
|
||||||
|
[SerializeField] private bool debugLogs = true;
|
||||||
|
|
||||||
|
[Header("Catalog")]
|
||||||
|
[Tooltip("디버그 HUD에서 검색 가능한 이상상태 목록")]
|
||||||
|
[SerializeField] private List<AbnormalityData> abnormalityCatalog = new List<AbnormalityData>();
|
||||||
|
|
||||||
|
private Rect windowRect = new Rect(20f, 20f, 420f, 520f);
|
||||||
|
private Vector2 catalogScroll;
|
||||||
|
private string abnormalityInput = string.Empty;
|
||||||
|
private string statusMessage = "이상상태 이름, 에셋 이름, 인덱스를 입력하세요.";
|
||||||
|
private bool isVisible;
|
||||||
|
private InputSystem_Actions inputActions;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (abnormalityManager == null)
|
||||||
|
abnormalityManager = GetComponent<AbnormalityManager>();
|
||||||
|
|
||||||
|
if (networkController == null)
|
||||||
|
networkController = GetComponent<PlayerNetworkController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
if (!IsOwner || !ShouldEnableDebugHud())
|
||||||
|
{
|
||||||
|
enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isVisible = showOnStart;
|
||||||
|
RefreshCatalog();
|
||||||
|
InitializeInputActions();
|
||||||
|
|
||||||
|
if (debugLogs)
|
||||||
|
{
|
||||||
|
Debug.Log("[AbnormalityDebugHUD] DebugHUD 액션으로 HUD를 열고, 이상상태 이름/에셋명/인덱스로 자신에게 적용할 수 있습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkDespawn()
|
||||||
|
{
|
||||||
|
CleanupInputActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGUI()
|
||||||
|
{
|
||||||
|
if (!IsOwner || !isVisible || !ShouldEnableDebugHud())
|
||||||
|
return;
|
||||||
|
|
||||||
|
windowRect = GUI.Window(GetInstanceID(), windowRect, DrawWindow, "Abnormality Debug HUD");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawWindow(int windowId)
|
||||||
|
{
|
||||||
|
GUILayout.BeginVertical();
|
||||||
|
|
||||||
|
GUILayout.Label($"사망 상태: {(networkController != null && networkController.IsDead ? "Dead" : "Alive")}");
|
||||||
|
GUILayout.Label("입력 예시: 기절 / Data_Abnormality_Player_Stun / 0");
|
||||||
|
|
||||||
|
GUI.SetNextControlName("AbnormalityInputField");
|
||||||
|
abnormalityInput = GUILayout.TextField(abnormalityInput ?? string.Empty);
|
||||||
|
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
if (GUILayout.Button("적용", GUILayout.Height(28f)))
|
||||||
|
{
|
||||||
|
ApplyFromInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GUILayout.Button("해제", GUILayout.Height(28f)))
|
||||||
|
{
|
||||||
|
RemoveFromInput();
|
||||||
|
}
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
if (GUILayout.Button("모두 해제", GUILayout.Height(24f)))
|
||||||
|
{
|
||||||
|
if (abnormalityManager != null)
|
||||||
|
{
|
||||||
|
abnormalityManager.RemoveAllAbnormalities();
|
||||||
|
SetStatus("활성 이상상태를 모두 해제했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GUILayout.Button("즉사", GUILayout.Height(24f)))
|
||||||
|
{
|
||||||
|
KillSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GUILayout.Button("리스폰", GUILayout.Height(24f)))
|
||||||
|
{
|
||||||
|
RequestRespawnRpc();
|
||||||
|
}
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
GUILayout.Space(6f);
|
||||||
|
GUILayout.Label($"상태: {statusMessage}");
|
||||||
|
GUILayout.Space(6f);
|
||||||
|
|
||||||
|
GUILayout.Label("활성 이상상태");
|
||||||
|
DrawActiveAbnormalities();
|
||||||
|
|
||||||
|
GUILayout.Space(6f);
|
||||||
|
GUILayout.Label("카탈로그");
|
||||||
|
catalogScroll = GUILayout.BeginScrollView(catalogScroll, GUILayout.Height(220f));
|
||||||
|
for (int i = 0; i < abnormalityCatalog.Count; i++)
|
||||||
|
{
|
||||||
|
AbnormalityData data = abnormalityCatalog[i];
|
||||||
|
if (data == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
GUILayout.Label($"[{i}] {data.abnormalityName} ({data.name})", GUILayout.Width(280f));
|
||||||
|
if (GUILayout.Button("적용", GUILayout.Width(50f)))
|
||||||
|
{
|
||||||
|
ApplyAbnormality(data);
|
||||||
|
}
|
||||||
|
if (GUILayout.Button("해제", GUILayout.Width(50f)))
|
||||||
|
{
|
||||||
|
RemoveAbnormality(data);
|
||||||
|
}
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
GUILayout.EndScrollView();
|
||||||
|
|
||||||
|
GUILayout.EndVertical();
|
||||||
|
GUI.DragWindow(new Rect(0f, 0f, 10000f, 20f));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (IsOwner && inputActions != null)
|
||||||
|
{
|
||||||
|
inputActions.Player.Enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
CleanupInputActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeInputActions()
|
||||||
|
{
|
||||||
|
if (inputActions == null)
|
||||||
|
{
|
||||||
|
inputActions = new InputSystem_Actions();
|
||||||
|
inputActions.Player.DebugHUD.performed += OnDebugHudPerformed;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputActions.Player.Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupInputActions()
|
||||||
|
{
|
||||||
|
if (inputActions != null)
|
||||||
|
{
|
||||||
|
inputActions.Player.Disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDebugHudPerformed(InputAction.CallbackContext context)
|
||||||
|
{
|
||||||
|
if (!IsOwner)
|
||||||
|
return;
|
||||||
|
|
||||||
|
isVisible = !isVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawActiveAbnormalities()
|
||||||
|
{
|
||||||
|
if (abnormalityManager == null || abnormalityManager.ActiveAbnormalities.Count == 0)
|
||||||
|
{
|
||||||
|
GUILayout.Label("- 없음");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < abnormalityManager.ActiveAbnormalities.Count; i++)
|
||||||
|
{
|
||||||
|
var active = abnormalityManager.ActiveAbnormalities[i];
|
||||||
|
if (active == null || active.Data == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string durationText = active.Data.IsPermanent ? "영구" : $"{active.RemainingDuration:F1}s";
|
||||||
|
GUILayout.Label($"- {active.Data.abnormalityName} / {durationText}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFromInput()
|
||||||
|
{
|
||||||
|
if (!TryResolveAbnormality(abnormalityInput, out AbnormalityData data, out string message))
|
||||||
|
{
|
||||||
|
SetStatus(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyAbnormality(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveFromInput()
|
||||||
|
{
|
||||||
|
if (!TryResolveAbnormality(abnormalityInput, out AbnormalityData data, out string message))
|
||||||
|
{
|
||||||
|
SetStatus(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveAbnormality(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyAbnormality(AbnormalityData data)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
SetStatus("적용할 이상상태를 찾지 못했습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abnormalityManager == null)
|
||||||
|
{
|
||||||
|
SetStatus("AbnormalityManager 참조가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
abnormalityManager.ApplyAbnormality(data, gameObject);
|
||||||
|
SetStatus($"'{data.abnormalityName}' 적용 요청을 보냈습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveAbnormality(AbnormalityData data)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
SetStatus("해제할 이상상태를 찾지 못했습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abnormalityManager == null)
|
||||||
|
{
|
||||||
|
SetStatus("AbnormalityManager 참조가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
abnormalityManager.RemoveAbnormality(data);
|
||||||
|
SetStatus($"'{data.abnormalityName}' 해제 요청을 보냈습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolveAbnormality(string input, out AbnormalityData resolved, out string message)
|
||||||
|
{
|
||||||
|
resolved = null;
|
||||||
|
message = string.Empty;
|
||||||
|
|
||||||
|
string normalizedInput = NormalizeIdentifier(input);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedInput))
|
||||||
|
{
|
||||||
|
message = "이상상태 이름, 에셋 이름, 인덱스를 입력하세요.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int.TryParse(normalizedInput, out int index))
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < abnormalityCatalog.Count && abnormalityCatalog[index] != null)
|
||||||
|
{
|
||||||
|
resolved = abnormalityCatalog[index];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = $"인덱스 {index} 에 해당하는 이상상태가 없습니다.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AbnormalityData> partialMatches = new List<AbnormalityData>();
|
||||||
|
for (int i = 0; i < abnormalityCatalog.Count; i++)
|
||||||
|
{
|
||||||
|
AbnormalityData candidate = abnormalityCatalog[i];
|
||||||
|
if (candidate == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string normalizedName = NormalizeIdentifier(candidate.abnormalityName);
|
||||||
|
string normalizedAssetName = NormalizeIdentifier(candidate.name);
|
||||||
|
|
||||||
|
if (normalizedInput == normalizedName || normalizedInput == normalizedAssetName)
|
||||||
|
{
|
||||||
|
resolved = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedName.Contains(normalizedInput) || normalizedAssetName.Contains(normalizedInput))
|
||||||
|
{
|
||||||
|
partialMatches.Add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partialMatches.Count == 1)
|
||||||
|
{
|
||||||
|
resolved = partialMatches[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partialMatches.Count > 1)
|
||||||
|
{
|
||||||
|
message = BuildAmbiguousMessage(partialMatches);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = $"'{input}' 에 해당하는 이상상태를 찾지 못했습니다.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildAmbiguousMessage(List<AbnormalityData> matches)
|
||||||
|
{
|
||||||
|
int previewCount = Mathf.Min(3, matches.Count);
|
||||||
|
List<string> previewNames = new List<string>(previewCount);
|
||||||
|
for (int i = 0; i < previewCount; i++)
|
||||||
|
{
|
||||||
|
AbnormalityData match = matches[i];
|
||||||
|
previewNames.Add(match != null ? match.abnormalityName : "null");
|
||||||
|
}
|
||||||
|
|
||||||
|
string preview = string.Join(", ", previewNames);
|
||||||
|
if (matches.Count > previewCount)
|
||||||
|
{
|
||||||
|
preview += ", ...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"여러 후보가 있습니다: {preview}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeIdentifier(string value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value)
|
||||||
|
? string.Empty
|
||||||
|
: value.Trim().Replace(" ", string.Empty).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetStatus(string message)
|
||||||
|
{
|
||||||
|
statusMessage = message;
|
||||||
|
|
||||||
|
if (debugLogs)
|
||||||
|
{
|
||||||
|
Debug.Log($"[AbnormalityDebugHUD] {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KillSelf()
|
||||||
|
{
|
||||||
|
if (networkController == null)
|
||||||
|
{
|
||||||
|
SetStatus("PlayerNetworkController 참조가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkController.IsDead)
|
||||||
|
{
|
||||||
|
SetStatus("이미 사망한 상태입니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkController.TakeDamageRpc(networkController.Health + 1f);
|
||||||
|
SetStatus("즉사 요청을 보냈습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Rpc(SendTo.Server)]
|
||||||
|
private void RequestRespawnRpc()
|
||||||
|
{
|
||||||
|
if (networkController == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
networkController.Respawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldEnableDebugHud()
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return Debug.isDebugBuild;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshCatalog()
|
||||||
|
{
|
||||||
|
abnormalityCatalog.RemoveAll(data => data == null);
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
string[] guids = AssetDatabase.FindAssets("t:AbnormalityData", new[] { "Assets/_Game/Data/Abnormalities" });
|
||||||
|
List<AbnormalityData> loadedAssets = new List<AbnormalityData>(guids.Length);
|
||||||
|
|
||||||
|
for (int i = 0; i < guids.Length; i++)
|
||||||
|
{
|
||||||
|
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||||
|
AbnormalityData data = AssetDatabase.LoadAssetAtPath<AbnormalityData>(path);
|
||||||
|
if (data != null)
|
||||||
|
{
|
||||||
|
loadedAssets.Add(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedAssets.Sort((left, right) =>
|
||||||
|
string.Compare(left != null ? left.name : string.Empty, right != null ? right.name : string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
abnormalityCatalog = loadedAssets;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bea222c7cf052d949984b6c08b08e545
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
using Unity.Netcode;
|
||||||
|
|
||||||
|
using Colosseum.Abnormalities;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Colosseum.Player
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 플레이어 이상상태와 행동 제어 연동을 자동 검증하는 디버그 러너.
|
||||||
|
/// 기절, 침묵, 사망, 리스폰 순으로 상태를 검사합니다.
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class PlayerAbnormalityVerificationRunner : NetworkBehaviour
|
||||||
|
{
|
||||||
|
[Header("References")]
|
||||||
|
[SerializeField] private AbnormalityManager abnormalityManager;
|
||||||
|
[SerializeField] private PlayerActionState actionState;
|
||||||
|
[SerializeField] private PlayerNetworkController networkController;
|
||||||
|
|
||||||
|
[Header("Test Data")]
|
||||||
|
[SerializeField] private AbnormalityData stunData;
|
||||||
|
[SerializeField] private AbnormalityData silenceData;
|
||||||
|
|
||||||
|
[Header("Execution")]
|
||||||
|
[Tooltip("에디터 플레이 시작 시 자동 검증 실행")]
|
||||||
|
[SerializeField] private bool runOnStartInEditor = false;
|
||||||
|
|
||||||
|
[Tooltip("각 검증 단계 사이 대기 시간")]
|
||||||
|
[Min(0.05f)]
|
||||||
|
[SerializeField] private float settleDelay = 0.2f;
|
||||||
|
|
||||||
|
[Header("Result")]
|
||||||
|
[SerializeField] private bool isRunning;
|
||||||
|
[SerializeField] private bool lastRunPassed;
|
||||||
|
[SerializeField] private int totalChecks;
|
||||||
|
[SerializeField] private int failedChecks;
|
||||||
|
[TextArea(5, 12)]
|
||||||
|
[SerializeField] private string lastReport = string.Empty;
|
||||||
|
|
||||||
|
private readonly System.Text.StringBuilder reportBuilder = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
if (!IsOwner || !ShouldEnableRunner())
|
||||||
|
{
|
||||||
|
enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResolveReferences();
|
||||||
|
LoadDefaultAssetsIfNeeded();
|
||||||
|
|
||||||
|
if (runOnStartInEditor)
|
||||||
|
{
|
||||||
|
StartCoroutine(RunVerificationRoutine());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ContextMenu("Run Verification")]
|
||||||
|
public void RunVerification()
|
||||||
|
{
|
||||||
|
if (!Application.isPlaying || !IsOwner || isRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
StartCoroutine(RunVerificationRoutine());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator RunVerificationRoutine()
|
||||||
|
{
|
||||||
|
if (isRunning)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
ResolveReferences();
|
||||||
|
LoadDefaultAssetsIfNeeded();
|
||||||
|
|
||||||
|
if (abnormalityManager == null || actionState == null || networkController == null || stunData == null || silenceData == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[AbnormalityVerification] Missing references or test data.");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning = true;
|
||||||
|
totalChecks = 0;
|
||||||
|
failedChecks = 0;
|
||||||
|
lastRunPassed = false;
|
||||||
|
reportBuilder.Clear();
|
||||||
|
AppendLine("=== Player Abnormality Verification Start ===");
|
||||||
|
|
||||||
|
abnormalityManager.RemoveAllAbnormalities();
|
||||||
|
RequestRespawnRpc();
|
||||||
|
yield return new WaitForSeconds(settleDelay);
|
||||||
|
|
||||||
|
Verify("초기 상태: 사망 아님", !networkController.IsDead);
|
||||||
|
Verify("초기 상태: 이동 가능", actionState.CanMove);
|
||||||
|
Verify("초기 상태: 스킬 사용 가능", actionState.CanUseSkills);
|
||||||
|
|
||||||
|
abnormalityManager.ApplyAbnormality(stunData, gameObject);
|
||||||
|
yield return new WaitForSeconds(settleDelay);
|
||||||
|
|
||||||
|
Verify("기절 적용: IsStunned", abnormalityManager.IsStunned);
|
||||||
|
Verify("기절 적용: ActionState.IsStunned", actionState.IsStunned);
|
||||||
|
Verify("기절 적용: 이동 불가", !actionState.CanMove);
|
||||||
|
Verify("기절 적용: 점프 불가", !actionState.CanJump);
|
||||||
|
Verify("기절 적용: 스킬 사용 불가", !actionState.CanUseSkills);
|
||||||
|
Verify("기절 적용: 이동속도 0", Mathf.Approximately(actionState.MoveSpeedMultiplier, 0f));
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(stunData.duration + settleDelay);
|
||||||
|
|
||||||
|
Verify("기절 해제: IsStunned false", !abnormalityManager.IsStunned);
|
||||||
|
Verify("기절 해제: 이동 가능 복구", actionState.CanMove);
|
||||||
|
Verify("기절 해제: 스킬 사용 가능 복구", actionState.CanUseSkills);
|
||||||
|
|
||||||
|
abnormalityManager.ApplyAbnormality(silenceData, gameObject);
|
||||||
|
yield return new WaitForSeconds(settleDelay);
|
||||||
|
|
||||||
|
Verify("침묵 적용: IsSilenced", abnormalityManager.IsSilenced);
|
||||||
|
Verify("침묵 적용: 이동 가능 유지", actionState.CanMove);
|
||||||
|
Verify("침묵 적용: 점프 가능 유지", actionState.CanJump);
|
||||||
|
Verify("침묵 적용: 스킬 사용 불가", !actionState.CanUseSkills);
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(silenceData.duration + settleDelay);
|
||||||
|
|
||||||
|
Verify("침묵 해제: IsSilenced false", !abnormalityManager.IsSilenced);
|
||||||
|
Verify("침묵 해제: 스킬 사용 가능 복구", actionState.CanUseSkills);
|
||||||
|
|
||||||
|
abnormalityManager.ApplyAbnormality(stunData, gameObject);
|
||||||
|
yield return new WaitForSeconds(settleDelay);
|
||||||
|
networkController.TakeDamageRpc(networkController.Health + 1f);
|
||||||
|
yield return new WaitForSeconds(settleDelay);
|
||||||
|
|
||||||
|
Verify("사망 처리: IsDead", networkController.IsDead);
|
||||||
|
Verify("사망 처리: 입력 불가", !actionState.CanReceiveInput);
|
||||||
|
Verify("사망 처리: 이동 불가", !actionState.CanMove);
|
||||||
|
Verify("사망 처리: 스킬 사용 불가", !actionState.CanUseSkills);
|
||||||
|
Verify("사망 처리: 활성 이상상태 제거", abnormalityManager.ActiveAbnormalities.Count == 0);
|
||||||
|
|
||||||
|
RequestRespawnRpc();
|
||||||
|
yield return new WaitForSeconds(settleDelay);
|
||||||
|
|
||||||
|
Verify("리스폰: IsDead false", !networkController.IsDead);
|
||||||
|
Verify("리스폰: 활성 이상상태 없음", abnormalityManager.ActiveAbnormalities.Count == 0);
|
||||||
|
Verify("리스폰: 이동 가능", actionState.CanMove);
|
||||||
|
Verify("리스폰: 스킬 사용 가능", actionState.CanUseSkills);
|
||||||
|
|
||||||
|
lastRunPassed = failedChecks == 0;
|
||||||
|
AppendLine(lastRunPassed
|
||||||
|
? "=== Verification Passed ==="
|
||||||
|
: $"=== Verification Failed: {failedChecks}/{totalChecks} checks failed ===");
|
||||||
|
|
||||||
|
lastReport = reportBuilder.ToString();
|
||||||
|
Debug.Log(lastReport);
|
||||||
|
isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Rpc(SendTo.Server)]
|
||||||
|
private void RequestRespawnRpc()
|
||||||
|
{
|
||||||
|
if (networkController == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
networkController.Respawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResolveReferences()
|
||||||
|
{
|
||||||
|
if (abnormalityManager == null)
|
||||||
|
abnormalityManager = GetComponent<AbnormalityManager>();
|
||||||
|
if (actionState == null)
|
||||||
|
actionState = GetComponent<PlayerActionState>();
|
||||||
|
if (networkController == null)
|
||||||
|
networkController = GetComponent<PlayerNetworkController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadDefaultAssetsIfNeeded()
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
if (stunData == null)
|
||||||
|
{
|
||||||
|
stunData = AssetDatabase.LoadAssetAtPath<AbnormalityData>("Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Stun.asset");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (silenceData == null)
|
||||||
|
{
|
||||||
|
silenceData = AssetDatabase.LoadAssetAtPath<AbnormalityData>("Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Silence.asset");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Verify(string label, bool condition)
|
||||||
|
{
|
||||||
|
totalChecks++;
|
||||||
|
if (!condition)
|
||||||
|
{
|
||||||
|
failedChecks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendLine($"{(condition ? "[PASS]" : "[FAIL]")} {label}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendLine(string text)
|
||||||
|
{
|
||||||
|
if (reportBuilder.Length > 0)
|
||||||
|
{
|
||||||
|
reportBuilder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
reportBuilder.Append(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldEnableRunner()
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return Debug.isDebugBuild;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 43d5dfd0218bf3445b8678dae42350d6
|
||||||
104
Assets/_Game/Scripts/Player/PlayerActionState.cs
Normal file
104
Assets/_Game/Scripts/Player/PlayerActionState.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
using Colosseum.Abnormalities;
|
||||||
|
using Colosseum.Skills;
|
||||||
|
|
||||||
|
namespace Colosseum.Player
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 플레이어의 전투 행동 가능 여부를 한 곳에서 판정하는 상태 관리자.
|
||||||
|
/// 이동, 점프, 스킬 사용 가능 여부를 사망/이상 상태/관전/시전 상태와 연동합니다.
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class PlayerActionState : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("References")]
|
||||||
|
[Tooltip("플레이어 네트워크 상태")]
|
||||||
|
[SerializeField] private PlayerNetworkController networkController;
|
||||||
|
|
||||||
|
[Tooltip("이상 상태 관리자")]
|
||||||
|
[SerializeField] private AbnormalityManager abnormalityManager;
|
||||||
|
|
||||||
|
[Tooltip("스킬 실행 관리자")]
|
||||||
|
[SerializeField] private SkillController skillController;
|
||||||
|
|
||||||
|
[Tooltip("관전 관리자")]
|
||||||
|
[SerializeField] private PlayerSpectator spectator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 사망 상태 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDead => networkController != null && networkController.IsDead;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기절 상태 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool IsStunned => abnormalityManager != null && abnormalityManager.IsStunned;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 침묵 상태 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSilenced => abnormalityManager != null && abnormalityManager.IsSilenced;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 관전 상태 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSpectating => spectator != null && spectator.IsSpectating;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 스킬 애니메이션 재생 중 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCastingSkill => skillController != null && skillController.IsPlayingAnimation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 입력을 받아도 되는지 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool CanReceiveInput => !IsDead && !IsSpectating;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 플레이어가 직접 이동 입력을 사용할 수 있는지 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool CanMove => CanReceiveInput && !IsStunned && !IsCastingSkill;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 점프 가능 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool CanJump => CanMove;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 스킬 사용 가능 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsSilenced && !IsCastingSkill;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 회피 사용 가능 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool CanEvade => CanUseSkills;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 이동 속도 배율
|
||||||
|
/// </summary>
|
||||||
|
public float MoveSpeedMultiplier
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!CanReceiveInput || IsStunned)
|
||||||
|
return 0f;
|
||||||
|
|
||||||
|
return abnormalityManager != null ? abnormalityManager.MoveSpeedMultiplier : 1f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (networkController == null)
|
||||||
|
networkController = GetComponent<PlayerNetworkController>();
|
||||||
|
if (abnormalityManager == null)
|
||||||
|
abnormalityManager = GetComponent<AbnormalityManager>();
|
||||||
|
if (skillController == null)
|
||||||
|
skillController = GetComponent<SkillController>();
|
||||||
|
if (spectator == null)
|
||||||
|
spectator = GetComponentInChildren<PlayerSpectator>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Game/Scripts/Player/PlayerActionState.cs.meta
Normal file
2
Assets/_Game/Scripts/Player/PlayerActionState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 01f7f1d2e296d2046b2977e94b7269fe
|
||||||
@@ -12,6 +12,7 @@ namespace Colosseum.Player
|
|||||||
/// - 서버: NetworkVariable을 읽어 CharacterController 구동 → NetworkTransform으로 동기화
|
/// - 서버: NetworkVariable을 읽어 CharacterController 구동 → NetworkTransform으로 동기화
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RequireComponent(typeof(CharacterController))]
|
[RequireComponent(typeof(CharacterController))]
|
||||||
|
[RequireComponent(typeof(PlayerActionState))]
|
||||||
public class PlayerMovement : NetworkBehaviour
|
public class PlayerMovement : NetworkBehaviour
|
||||||
{
|
{
|
||||||
[Header("Movement Settings")]
|
[Header("Movement Settings")]
|
||||||
@@ -25,6 +26,7 @@ namespace Colosseum.Player
|
|||||||
[Header("References")]
|
[Header("References")]
|
||||||
[SerializeField] private SkillController skillController;
|
[SerializeField] private SkillController skillController;
|
||||||
[SerializeField] private Animator animator;
|
[SerializeField] private Animator animator;
|
||||||
|
[SerializeField] private PlayerActionState actionState;
|
||||||
|
|
||||||
private CharacterController characterController;
|
private CharacterController characterController;
|
||||||
private Vector3 velocity;
|
private Vector3 velocity;
|
||||||
@@ -43,7 +45,7 @@ namespace Colosseum.Player
|
|||||||
private Vector3 blockedDirection;
|
private Vector3 blockedDirection;
|
||||||
private readonly Collider[] overlapBuffer = new Collider[8];
|
private readonly Collider[] overlapBuffer = new Collider[8];
|
||||||
|
|
||||||
public float CurrentMoveSpeed => netMoveInput.Value.magnitude * moveSpeed;
|
public float CurrentMoveSpeed => netMoveInput.Value.magnitude * moveSpeed * GetMoveSpeedMultiplier();
|
||||||
public bool IsGrounded => characterController != null ? characterController.isGrounded : false;
|
public bool IsGrounded => characterController != null ? characterController.isGrounded : false;
|
||||||
public bool IsJumping => isJumping;
|
public bool IsJumping => isJumping;
|
||||||
|
|
||||||
@@ -62,6 +64,8 @@ namespace Colosseum.Player
|
|||||||
skillController = GetComponent<SkillController>();
|
skillController = GetComponent<SkillController>();
|
||||||
if (animator == null)
|
if (animator == null)
|
||||||
animator = GetComponentInChildren<Animator>();
|
animator = GetComponentInChildren<Animator>();
|
||||||
|
if (actionState == null)
|
||||||
|
actionState = GetOrCreateActionState();
|
||||||
|
|
||||||
SetSpawnPosition();
|
SetSpawnPosition();
|
||||||
}
|
}
|
||||||
@@ -69,6 +73,9 @@ namespace Colosseum.Player
|
|||||||
// 오너: 입력 및 카메라 초기화
|
// 오너: 입력 및 카메라 초기화
|
||||||
if (IsOwner)
|
if (IsOwner)
|
||||||
{
|
{
|
||||||
|
if (actionState == null)
|
||||||
|
actionState = GetOrCreateActionState();
|
||||||
|
|
||||||
InitializeInputActions();
|
InitializeInputActions();
|
||||||
SetupCamera();
|
SetupCamera();
|
||||||
}
|
}
|
||||||
@@ -144,6 +151,8 @@ namespace Colosseum.Player
|
|||||||
private void OnJumpPerformed(InputAction.CallbackContext context)
|
private void OnJumpPerformed(InputAction.CallbackContext context)
|
||||||
{
|
{
|
||||||
if (!IsOwner) return;
|
if (!IsOwner) return;
|
||||||
|
if (actionState != null && !actionState.CanJump) return;
|
||||||
|
|
||||||
JumpRequestRpc();
|
JumpRequestRpc();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +162,9 @@ namespace Colosseum.Player
|
|||||||
[Rpc(SendTo.Server)]
|
[Rpc(SendTo.Server)]
|
||||||
private void JumpRequestRpc()
|
private void JumpRequestRpc()
|
||||||
{
|
{
|
||||||
|
if (actionState != null && !actionState.CanJump)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!isJumping && characterController != null && characterController.isGrounded)
|
if (!isJumping && characterController != null && characterController.isGrounded)
|
||||||
Jump();
|
Jump();
|
||||||
}
|
}
|
||||||
@@ -187,6 +199,14 @@ namespace Colosseum.Player
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void UpdateNetworkInput()
|
private void UpdateNetworkInput()
|
||||||
{
|
{
|
||||||
|
if (actionState != null && !actionState.CanMove)
|
||||||
|
{
|
||||||
|
if (netMoveInput.Value != Vector2.zero)
|
||||||
|
netMoveInput.Value = Vector2.zero;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Vector3 dir = new Vector3(moveInput.x, 0f, moveInput.y);
|
Vector3 dir = new Vector3(moveInput.x, 0f, moveInput.y);
|
||||||
if (dir.sqrMagnitude > 0.001f)
|
if (dir.sqrMagnitude > 0.001f)
|
||||||
dir = TransformDirectionByCamera(dir).normalized;
|
dir = TransformDirectionByCamera(dir).normalized;
|
||||||
@@ -251,10 +271,14 @@ namespace Colosseum.Player
|
|||||||
if (moveDirection.sqrMagnitude > 0.001f)
|
if (moveDirection.sqrMagnitude > 0.001f)
|
||||||
moveDirection.Normalize();
|
moveDirection.Normalize();
|
||||||
|
|
||||||
|
if (actionState != null && !actionState.CanMove)
|
||||||
|
moveDirection = Vector3.zero;
|
||||||
|
|
||||||
if (blockedDirection != Vector3.zero && Vector3.Dot(moveDirection, blockedDirection) > 0f)
|
if (blockedDirection != Vector3.zero && Vector3.Dot(moveDirection, blockedDirection) > 0f)
|
||||||
moveDirection = Vector3.zero;
|
moveDirection = Vector3.zero;
|
||||||
|
|
||||||
characterController.Move((moveDirection * moveSpeed + velocity) * Time.deltaTime);
|
float actualMoveSpeed = moveSpeed * GetMoveSpeedMultiplier();
|
||||||
|
characterController.Move((moveDirection * actualMoveSpeed + velocity) * Time.deltaTime);
|
||||||
|
|
||||||
if (moveDirection != Vector3.zero)
|
if (moveDirection != Vector3.zero)
|
||||||
{
|
{
|
||||||
@@ -291,6 +315,23 @@ namespace Colosseum.Player
|
|||||||
return right * direction.x + forward * direction.z;
|
return right * direction.x + forward * direction.z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float GetMoveSpeedMultiplier()
|
||||||
|
{
|
||||||
|
if (actionState == null)
|
||||||
|
return 1f;
|
||||||
|
|
||||||
|
return actionState.MoveSpeedMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayerActionState GetOrCreateActionState()
|
||||||
|
{
|
||||||
|
var foundState = GetComponent<PlayerActionState>();
|
||||||
|
if (foundState != null)
|
||||||
|
return foundState;
|
||||||
|
|
||||||
|
return gameObject.AddComponent<PlayerActionState>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 루트 모션 처리 (서버 전용 — NetworkTransform으로 동기화)
|
/// 루트 모션 처리 (서버 전용 — NetworkTransform으로 동기화)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
|
|
||||||
|
using Colosseum.Abnormalities;
|
||||||
using Colosseum.Stats;
|
using Colosseum.Stats;
|
||||||
using Colosseum.Combat;
|
using Colosseum.Combat;
|
||||||
|
using Colosseum.Skills;
|
||||||
|
|
||||||
namespace Colosseum.Player
|
namespace Colosseum.Player
|
||||||
{
|
{
|
||||||
@@ -33,6 +36,7 @@ namespace Colosseum.Player
|
|||||||
// 사망 이벤트
|
// 사망 이벤트
|
||||||
public event Action<PlayerNetworkController> OnDeath;
|
public event Action<PlayerNetworkController> OnDeath;
|
||||||
public event Action<bool> OnDeathStateChanged; // (isDead)
|
public event Action<bool> OnDeathStateChanged; // (isDead)
|
||||||
|
public event Action<PlayerNetworkController> OnRespawned;
|
||||||
|
|
||||||
// IDamageable 구현
|
// IDamageable 구현
|
||||||
public float CurrentHealth => currentHealth.Value;
|
public float CurrentHealth => currentHealth.Value;
|
||||||
@@ -105,6 +109,8 @@ namespace Colosseum.Player
|
|||||||
[Rpc(SendTo.Server)]
|
[Rpc(SendTo.Server)]
|
||||||
public void UseManaRpc(float amount)
|
public void UseManaRpc(float amount)
|
||||||
{
|
{
|
||||||
|
if (isDead.Value) return;
|
||||||
|
|
||||||
currentMana.Value = Mathf.Max(0f, currentMana.Value - amount);
|
currentMana.Value = Mathf.Max(0f, currentMana.Value - amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +120,8 @@ namespace Colosseum.Player
|
|||||||
[Rpc(SendTo.Server)]
|
[Rpc(SendTo.Server)]
|
||||||
public void RestoreHealthRpc(float amount)
|
public void RestoreHealthRpc(float amount)
|
||||||
{
|
{
|
||||||
|
if (isDead.Value) return;
|
||||||
|
|
||||||
currentHealth.Value = Mathf.Min(MaxHealth, currentHealth.Value + amount);
|
currentHealth.Value = Mathf.Min(MaxHealth, currentHealth.Value + amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +131,8 @@ namespace Colosseum.Player
|
|||||||
[Rpc(SendTo.Server)]
|
[Rpc(SendTo.Server)]
|
||||||
public void RestoreManaRpc(float amount)
|
public void RestoreManaRpc(float amount)
|
||||||
{
|
{
|
||||||
|
if (isDead.Value) return;
|
||||||
|
|
||||||
currentMana.Value = Mathf.Min(MaxMana, currentMana.Value + amount);
|
currentMana.Value = Mathf.Min(MaxMana, currentMana.Value + amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +158,13 @@ namespace Colosseum.Player
|
|||||||
|
|
||||||
isDead.Value = true;
|
isDead.Value = true;
|
||||||
|
|
||||||
|
// 사망 시 활성 이상 상태를 정리해 리스폰 시 잔존하지 않게 합니다.
|
||||||
|
var abnormalityManager = GetComponent<AbnormalityManager>();
|
||||||
|
if (abnormalityManager != null)
|
||||||
|
{
|
||||||
|
abnormalityManager.RemoveAllAbnormalities();
|
||||||
|
}
|
||||||
|
|
||||||
// 이동 비활성화
|
// 이동 비활성화
|
||||||
var movement = GetComponent<PlayerMovement>();
|
var movement = GetComponent<PlayerMovement>();
|
||||||
if (movement != null)
|
if (movement != null)
|
||||||
@@ -162,6 +179,13 @@ namespace Colosseum.Player
|
|||||||
skillInput.enabled = false;
|
skillInput.enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 실행 중인 스킬 즉시 취소
|
||||||
|
var skillController = GetComponent<SkillController>();
|
||||||
|
if (skillController != null)
|
||||||
|
{
|
||||||
|
skillController.CancelSkill();
|
||||||
|
}
|
||||||
|
|
||||||
// 모든 클라이언트에서 사망 애니메이션 재생
|
// 모든 클라이언트에서 사망 애니메이션 재생
|
||||||
PlayDeathAnimationRpc();
|
PlayDeathAnimationRpc();
|
||||||
|
|
||||||
@@ -178,6 +202,12 @@ namespace Colosseum.Player
|
|||||||
{
|
{
|
||||||
if (!IsServer) return;
|
if (!IsServer) return;
|
||||||
|
|
||||||
|
var abnormalityManager = GetComponent<AbnormalityManager>();
|
||||||
|
if (abnormalityManager != null)
|
||||||
|
{
|
||||||
|
abnormalityManager.RemoveAllAbnormalities();
|
||||||
|
}
|
||||||
|
|
||||||
isDead.Value = false;
|
isDead.Value = false;
|
||||||
currentHealth.Value = MaxHealth;
|
currentHealth.Value = MaxHealth;
|
||||||
currentMana.Value = MaxMana;
|
currentMana.Value = MaxMana;
|
||||||
@@ -203,6 +233,8 @@ namespace Colosseum.Player
|
|||||||
animator.Rebind();
|
animator.Rebind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OnRespawned?.Invoke(this);
|
||||||
|
|
||||||
Debug.Log($"[Player] Player {OwnerClientId} respawned!");
|
Debug.Log($"[Player] Player {OwnerClientId} respawned!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace Colosseum.Player
|
|||||||
/// 플레이어 스킬 입력 처리.
|
/// 플레이어 스킬 입력 처리.
|
||||||
/// 논타겟 방식: 입력 시 즉시 스킬 시전
|
/// 논타겟 방식: 입력 시 즉시 스킬 시전
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(PlayerActionState))]
|
||||||
public class PlayerSkillInput : NetworkBehaviour
|
public class PlayerSkillInput : NetworkBehaviour
|
||||||
{
|
{
|
||||||
[Header("Skill Slots")]
|
[Header("Skill Slots")]
|
||||||
@@ -24,6 +25,8 @@ namespace Colosseum.Player
|
|||||||
[SerializeField] private PlayerNetworkController networkController;
|
[SerializeField] private PlayerNetworkController networkController;
|
||||||
[Tooltip("WeaponEquipment (없으면 자동 검색)")]
|
[Tooltip("WeaponEquipment (없으면 자동 검색)")]
|
||||||
[SerializeField] private WeaponEquipment weaponEquipment;
|
[SerializeField] private WeaponEquipment weaponEquipment;
|
||||||
|
[Tooltip("행동 상태 관리자 (없으면 자동 검색)")]
|
||||||
|
[SerializeField] private PlayerActionState actionState;
|
||||||
|
|
||||||
private InputSystem_Actions inputActions;
|
private InputSystem_Actions inputActions;
|
||||||
|
|
||||||
@@ -61,36 +64,54 @@ namespace Colosseum.Player
|
|||||||
weaponEquipment = GetComponent<WeaponEquipment>();
|
weaponEquipment = GetComponent<WeaponEquipment>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actionState == null)
|
||||||
|
{
|
||||||
|
actionState = GetOrCreateActionState();
|
||||||
|
}
|
||||||
|
|
||||||
InitializeInputActions();
|
InitializeInputActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeInputActions()
|
private void InitializeInputActions()
|
||||||
{
|
{
|
||||||
inputActions = new InputSystem_Actions();
|
if (inputActions == null)
|
||||||
inputActions.Player.Enable();
|
{
|
||||||
|
inputActions = new InputSystem_Actions();
|
||||||
|
inputActions.Player.Skill1.performed += OnSkill1Performed;
|
||||||
|
inputActions.Player.Skill2.performed += OnSkill2Performed;
|
||||||
|
inputActions.Player.Skill3.performed += OnSkill3Performed;
|
||||||
|
inputActions.Player.Skill4.performed += OnSkill4Performed;
|
||||||
|
inputActions.Player.Skill5.performed += OnSkill5Performed;
|
||||||
|
inputActions.Player.Skill6.performed += OnSkill6Performed;
|
||||||
|
inputActions.Player.Evade.performed += OnEvadePerformed;
|
||||||
|
}
|
||||||
|
|
||||||
// 스킬 액션 콜백 등록
|
inputActions.Player.Enable();
|
||||||
inputActions.Player.Skill1.performed += _ => OnSkillInput(0);
|
|
||||||
inputActions.Player.Skill2.performed += _ => OnSkillInput(1);
|
|
||||||
inputActions.Player.Skill3.performed += _ => OnSkillInput(2);
|
|
||||||
inputActions.Player.Skill4.performed += _ => OnSkillInput(3);
|
|
||||||
inputActions.Player.Skill5.performed += _ => OnSkillInput(4);
|
|
||||||
inputActions.Player.Skill6.performed += _ => OnSkillInput(5);
|
|
||||||
inputActions.Player.Evade.performed += _ => OnSkillInput(6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNetworkDespawn()
|
public override void OnNetworkDespawn()
|
||||||
|
{
|
||||||
|
CleanupInputActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
CleanupInputActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (IsOwner && inputActions != null)
|
||||||
|
{
|
||||||
|
inputActions.Player.Enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupInputActions()
|
||||||
{
|
{
|
||||||
if (inputActions != null)
|
if (inputActions != null)
|
||||||
{
|
{
|
||||||
inputActions.Player.Skill1.performed -= _ => OnSkillInput(0);
|
inputActions.Player.Disable();
|
||||||
inputActions.Player.Skill2.performed -= _ => OnSkillInput(1);
|
|
||||||
inputActions.Player.Skill3.performed -= _ => OnSkillInput(2);
|
|
||||||
inputActions.Player.Skill4.performed -= _ => OnSkillInput(3);
|
|
||||||
inputActions.Player.Skill5.performed -= _ => OnSkillInput(4);
|
|
||||||
inputActions.Player.Skill6.performed -= _ => OnSkillInput(5);
|
|
||||||
inputActions.Player.Evade.performed -= _ => OnSkillInput(6);
|
|
||||||
inputActions.Disable();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +131,7 @@ namespace Colosseum.Player
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 사망 상태 체크
|
// 사망 상태 체크
|
||||||
if (networkController != null && networkController.IsDead)
|
if (actionState != null && !actionState.CanUseSkills)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// 로컬 체크 (빠른 피드백용)
|
// 로컬 체크 (빠른 피드백용)
|
||||||
@@ -152,7 +173,7 @@ namespace Colosseum.Player
|
|||||||
|
|
||||||
// 서버에서 다시 검증
|
// 서버에서 다시 검증
|
||||||
// 사망 상태 체크
|
// 사망 상태 체크
|
||||||
if (networkController != null && networkController.IsDead)
|
if (actionState != null && !actionState.CanUseSkills)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (skillController.IsExecutingSkill || skillController.IsOnCooldown(skill))
|
if (skillController.IsExecutingSkill || skillController.IsOnCooldown(skill))
|
||||||
@@ -242,7 +263,33 @@ namespace Colosseum.Player
|
|||||||
SkillData skill = GetSkill(slotIndex);
|
SkillData skill = GetSkill(slotIndex);
|
||||||
if (skill == null) return false;
|
if (skill == null) return false;
|
||||||
|
|
||||||
|
if (actionState != null && !actionState.CanUseSkills)
|
||||||
|
return false;
|
||||||
|
|
||||||
return !skillController.IsOnCooldown(skill) && !skillController.IsExecutingSkill;
|
return !skillController.IsOnCooldown(skill) && !skillController.IsExecutingSkill;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnSkill1Performed(InputAction.CallbackContext context) => OnSkillInput(0);
|
||||||
|
|
||||||
|
private void OnSkill2Performed(InputAction.CallbackContext context) => OnSkillInput(1);
|
||||||
|
|
||||||
|
private void OnSkill3Performed(InputAction.CallbackContext context) => OnSkillInput(2);
|
||||||
|
|
||||||
|
private void OnSkill4Performed(InputAction.CallbackContext context) => OnSkillInput(3);
|
||||||
|
|
||||||
|
private void OnSkill5Performed(InputAction.CallbackContext context) => OnSkillInput(4);
|
||||||
|
|
||||||
|
private void OnSkill6Performed(InputAction.CallbackContext context) => OnSkillInput(5);
|
||||||
|
|
||||||
|
private void OnEvadePerformed(InputAction.CallbackContext context) => OnSkillInput(6);
|
||||||
|
|
||||||
|
private PlayerActionState GetOrCreateActionState()
|
||||||
|
{
|
||||||
|
var foundState = GetComponent<PlayerActionState>();
|
||||||
|
if (foundState != null)
|
||||||
|
return foundState;
|
||||||
|
|
||||||
|
return gameObject.AddComponent<PlayerActionState>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ namespace Colosseum.UI
|
|||||||
[SerializeField] private string ipAddress = "127.0.0.1";
|
[SerializeField] private string ipAddress = "127.0.0.1";
|
||||||
[SerializeField] private ushort port = 7777;
|
[SerializeField] private ushort port = 7777;
|
||||||
|
|
||||||
|
[Header("Editor Test")]
|
||||||
|
[Tooltip("에디터 Play Mode 진입 시 자동으로 Host를 시작합니다")]
|
||||||
|
[SerializeField] private bool autoStartHostInEditor = false;
|
||||||
|
|
||||||
[Header("Status (Read Only)")]
|
[Header("Status (Read Only)")]
|
||||||
[SerializeField, Tooltip("현재 연결 상태")] private string connectionStatus = "Disconnected";
|
[SerializeField, Tooltip("현재 연결 상태")] private string connectionStatus = "Disconnected";
|
||||||
|
|
||||||
@@ -26,6 +30,13 @@ namespace Colosseum.UI
|
|||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
UpdateTransportSettings();
|
UpdateTransportSettings();
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
if (autoStartHostInEditor && NetworkManager.Singleton != null && !NetworkManager.Singleton.IsListening)
|
||||||
|
{
|
||||||
|
StartHost();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
|
|||||||
Reference in New Issue
Block a user