feat: 플레이어 다운/넉백 피격 반응 추가
This commit is contained in:
@@ -2491,15 +2491,15 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 170188453522781500, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
|
||||
propertyPath: m_AnchorMax.x
|
||||
value: 0
|
||||
value: 0.2
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 170188453522781500, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
value: 0
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 170188453522781500, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
|
||||
propertyPath: m_AnchorMin.x
|
||||
value: 0
|
||||
value: 0.2
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 222507439395443271, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
|
||||
propertyPath: m_Enabled
|
||||
@@ -2579,15 +2579,15 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 2668790415109567114, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
|
||||
propertyPath: m_AnchorMax.x
|
||||
value: 0
|
||||
value: 0.2
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 2668790415109567114, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
value: 0
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 2668790415109567114, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
|
||||
propertyPath: m_AnchorMin.x
|
||||
value: 0
|
||||
value: 0.2
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3299919758736932218, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
|
||||
propertyPath: m_SizeDelta.x
|
||||
@@ -4738,7 +4738,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.UI.ConnectionUI
|
||||
ipAddress: 127.0.0.1
|
||||
port: 7788
|
||||
port: 7777
|
||||
autoStartHostInEditor: 0
|
||||
connectionStatus: Disconnected
|
||||
--- !u!4 &854739757 stripped
|
||||
@@ -7252,6 +7252,18 @@ PrefabInstance:
|
||||
propertyPath: m_AnchorMax.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7035590099291789281, guid: 2122b1e1b36684a40978673f272f200e, type: 3}
|
||||
propertyPath: m_AnchorMax.x
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7035590099291789281, guid: 2122b1e1b36684a40978673f272f200e, type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7035590099291789281, guid: 2122b1e1b36684a40978673f272f200e, type: 3}
|
||||
propertyPath: m_AnchorMin.x
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
|
||||
BIN
Assets/_Game/Animations/Anim_Common_경직.fbx
Normal file
BIN
Assets/_Game/Animations/Anim_Common_경직.fbx
Normal file
Binary file not shown.
1564
Assets/_Game/Animations/Anim_Common_경직.fbx.meta
Normal file
1564
Assets/_Game/Animations/Anim_Common_경직.fbx.meta
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Assets/_Game/Animations/Anim_Common_다운루프.fbx
Normal file
BIN
Assets/_Game/Animations/Anim_Common_다운루프.fbx
Normal file
Binary file not shown.
1564
Assets/_Game/Animations/Anim_Common_다운루프.fbx.meta
Normal file
1564
Assets/_Game/Animations/Anim_Common_다운루프.fbx.meta
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Assets/_Game/Animations/Anim_Common_다운시작.fbx
Normal file
BIN
Assets/_Game/Animations/Anim_Common_다운시작.fbx
Normal file
Binary file not shown.
1564
Assets/_Game/Animations/Anim_Common_다운시작.fbx.meta
Normal file
1564
Assets/_Game/Animations/Anim_Common_다운시작.fbx.meta
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Assets/_Game/Animations/Anim_Common_다운회복.fbx
Normal file
BIN
Assets/_Game/Animations/Anim_Common_다운회복.fbx
Normal file
Binary file not shown.
1564
Assets/_Game/Animations/Anim_Common_다운회복.fbx.meta
Normal file
1564
Assets/_Game/Animations/Anim_Common_다운회복.fbx.meta
Normal file
File diff suppressed because it is too large
Load Diff
@@ -79,11 +79,21 @@ AnimatorStateMachine:
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: -1032254959699123210}
|
||||
m_Position: {x: -450, y: 270, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 1111345123456789012}
|
||||
m_Position: {x: -700, y: 140, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 1111345123456789013}
|
||||
m_Position: {x: -980, y: 140, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 1111345123456789014}
|
||||
m_Position: {x: -1260, y: 140, z: 0}
|
||||
m_ChildStateMachines: []
|
||||
m_AnyStateTransitions:
|
||||
- {fileID: -754003289131015157}
|
||||
- {fileID: 469741948129995159}
|
||||
- {fileID: 6228136561094308872}
|
||||
- {fileID: 1111345123456789021}
|
||||
m_EntryTransitions: []
|
||||
m_StateMachineTransitions: {}
|
||||
m_StateMachineBehaviours: []
|
||||
@@ -323,6 +333,18 @@ AnimatorController:
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
- m_Name: Down
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
- m_Name: Recover
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
m_AnimatorLayers:
|
||||
- serializedVersion: 5
|
||||
m_Name: Base Layer
|
||||
@@ -437,6 +459,181 @@ AnimatorStateTransition:
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &1111345123456789012
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: DownBegin
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: 1111345123456789022}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 3325809351021969952, guid: 9f1ccf176a8592d4997f8a7c48f9571f, type: 3}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1102 &1111345123456789013
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: DownLoop
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: 1111345123456789023}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 4349338778344460676, guid: b0e7c1111468a924a81b4323e5aa8eb4, type: 3}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1102 &1111345123456789014
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Recover
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: 1111345123456789024}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: -3512901111006340046, guid: 3866152d4c1922849be562e6e814dce3, type: 3}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1101 &1111345123456789021
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: Down
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 1111345123456789012}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.05
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1101 &1111345123456789022
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 1111345123456789013}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.05
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.95
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1101 &1111345123456789023
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: Recover
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 1111345123456789014}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.05
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1101 &1111345123456789024
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: -7908033645098541312}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.1
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.95
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &7693173606830535998
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
|
||||
@@ -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: 41c96b54a96cdb84c9bda774775b0a1a, type: 3}
|
||||
m_Name: "Data_SkillEffect_Player_\uB2E4\uC6B4"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DownEffect
|
||||
targetType: 0
|
||||
targetTeam: 0
|
||||
areaCenter: 0
|
||||
areaShape: 0
|
||||
targetLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
areaRadius: 3
|
||||
fanOriginDistance: 1
|
||||
fanRadius: 3
|
||||
fanHalfAngle: 45
|
||||
duration: 1
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e95f5df9caf4134d956c476bb8079e0
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -20,7 +20,6 @@ GameObject:
|
||||
- component: {fileID: 11400005}
|
||||
- component: {fileID: 11400006}
|
||||
- component: {fileID: 3792588902782784034}
|
||||
- component: {fileID: 9069822911508997612}
|
||||
m_Layer: 0
|
||||
m_Name: Prefab_Boss_BossTemplate
|
||||
m_TagString: Untagged
|
||||
@@ -291,23 +290,3 @@ MonoBehaviour:
|
||||
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
|
||||
|
||||
@@ -243,6 +243,14 @@ MonoBehaviour:
|
||||
NameHash: 20298039
|
||||
Synchronize: 1
|
||||
ParameterType: 9
|
||||
- name: Down
|
||||
NameHash: -1127399675
|
||||
Synchronize: 1
|
||||
ParameterType: 9
|
||||
- name: Recover
|
||||
NameHash: 976603345
|
||||
Synchronize: 1
|
||||
ParameterType: 9
|
||||
AnimatorParametersExpanded: 0
|
||||
--- !u!95 &3426985706796420257
|
||||
Animator:
|
||||
@@ -305,6 +313,8 @@ MonoBehaviour:
|
||||
debugMode: 1
|
||||
showAreaDebug: 1
|
||||
debugDrawDuration: 1
|
||||
lastCancelledSkillName:
|
||||
lastCancelReason: 0
|
||||
--- !u!114 &6585367215453362640
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -345,6 +355,7 @@ MonoBehaviour:
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
characterStats: {fileID: -5132198055668300151}
|
||||
networkController: {fileID: 1664515335065415329}
|
||||
skillController: {fileID: 0}
|
||||
--- !u!114 &3552488436187204500
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -4,6 +4,7 @@ using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
using Colosseum.Stats;
|
||||
using Colosseum.Player;
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Abnormalities
|
||||
{
|
||||
@@ -20,6 +21,9 @@ namespace Colosseum.Abnormalities
|
||||
[Tooltip("PlayerNetworkController 컴포넌트 (HP/MP 관리용)")]
|
||||
[SerializeField] private PlayerNetworkController networkController;
|
||||
|
||||
[Tooltip("스킬 실행 관리자 (강제 취소 처리용)")]
|
||||
[SerializeField] private SkillController skillController;
|
||||
|
||||
// 활성화된 이상 상태 목록
|
||||
private readonly List<ActiveAbnormality> activeAbnormalities = new List<ActiveAbnormality>();
|
||||
|
||||
@@ -108,6 +112,9 @@ namespace Colosseum.Abnormalities
|
||||
if (networkController == null)
|
||||
networkController = GetComponent<PlayerNetworkController>();
|
||||
|
||||
if (skillController == null)
|
||||
skillController = GetComponent<SkillController>();
|
||||
|
||||
syncedAbnormalities = new NetworkList<AbnormalitySyncData>();
|
||||
}
|
||||
|
||||
@@ -414,9 +421,12 @@ namespace Colosseum.Abnormalities
|
||||
|
||||
private void ApplyControlEffect(AbnormalityData data)
|
||||
{
|
||||
bool enteredStun = false;
|
||||
|
||||
switch (data.controlType)
|
||||
{
|
||||
case ControlType.Stun:
|
||||
enteredStun = stunCount == 0;
|
||||
stunCount++;
|
||||
break;
|
||||
|
||||
@@ -434,6 +444,11 @@ namespace Colosseum.Abnormalities
|
||||
}
|
||||
|
||||
SyncControlEffects();
|
||||
|
||||
if (enteredStun)
|
||||
{
|
||||
TryCancelCurrentSkill(SkillCancelReason.Stun, data.abnormalityName);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveControlEffect(AbnormalityData data)
|
||||
@@ -584,6 +599,17 @@ namespace Colosseum.Abnormalities
|
||||
Debug.Log($"[Abnormality] Cleared all abnormalities on death: {gameObject.name}");
|
||||
}
|
||||
|
||||
private void TryCancelCurrentSkill(SkillCancelReason reason, string sourceName)
|
||||
{
|
||||
if (!IsServer || skillController == null || !skillController.IsPlayingAnimation)
|
||||
return;
|
||||
|
||||
if (skillController.CancelSkill(reason))
|
||||
{
|
||||
Debug.Log($"[Abnormality] Cancelled skill because '{sourceName}' applied to {gameObject.name}");
|
||||
}
|
||||
}
|
||||
|
||||
private AbnormalityData FindAbnormalityDataById(int instanceId)
|
||||
{
|
||||
var allData = Resources.FindObjectsOfTypeAll<AbnormalityData>();
|
||||
|
||||
@@ -7,6 +7,7 @@ using Unity.Netcode;
|
||||
|
||||
using Colosseum.Stats;
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Enemy
|
||||
{
|
||||
@@ -347,7 +348,7 @@ namespace Colosseum.Enemy
|
||||
var skillController = GetComponent<Colosseum.Skills.SkillController>();
|
||||
if (skillController != null)
|
||||
{
|
||||
skillController.CancelSkill();
|
||||
skillController.CancelSkill(SkillCancelReason.Death);
|
||||
}
|
||||
|
||||
// 모든 클라이언트에서 사망 애니메이션 재생
|
||||
@@ -383,6 +384,12 @@ namespace Colosseum.Enemy
|
||||
{
|
||||
animator.Rebind();
|
||||
}
|
||||
|
||||
var skillController = GetComponent<SkillController>();
|
||||
if (skillController != null)
|
||||
{
|
||||
skillController.CancelSkill(SkillCancelReason.Respawn);
|
||||
}
|
||||
}
|
||||
|
||||
// 체력 변화 이벤트 전파
|
||||
|
||||
4
Assets/_Game/Scripts/Network/EditorPlayHostBootstrap.cs
Normal file
4
Assets/_Game/Scripts/Network/EditorPlayHostBootstrap.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace Colosseum.Network
|
||||
{
|
||||
// 임시 삭제 후 Unity 컴파일 캐시 정리를 위한 빈 파일
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f55c7f03b6f5b6e448b8d03880306bb3
|
||||
211
Assets/_Game/Scripts/Player/HitReactionController.cs
Normal file
211
Assets/_Game/Scripts/Player/HitReactionController.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using UnityEngine;
|
||||
|
||||
using Unity.Netcode;
|
||||
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 플레이어의 피격 제어 상태를 관리합니다.
|
||||
/// 넉백 강제 이동과 다운 상태, 피격 애니메이션 재생을 담당합니다.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(PlayerMovement))]
|
||||
public class HitReactionController : NetworkBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[Tooltip("플레이어 이동 컴포넌트")]
|
||||
[SerializeField] private PlayerMovement playerMovement;
|
||||
|
||||
[Tooltip("스킬 실행 관리자")]
|
||||
[SerializeField] private SkillController skillController;
|
||||
|
||||
[Tooltip("플레이어 네트워크 상태")]
|
||||
[SerializeField] private PlayerNetworkController networkController;
|
||||
|
||||
[Tooltip("피격 애니메이션을 재생할 Animator")]
|
||||
[SerializeField] private Animator animator;
|
||||
|
||||
[Header("Animation")]
|
||||
[Tooltip("일반 피격 트리거 이름")]
|
||||
[SerializeField] private string hitTriggerParam = "Hit";
|
||||
|
||||
[Tooltip("다운 시작 트리거 이름")]
|
||||
[SerializeField] private string downTriggerParam = "Down";
|
||||
|
||||
[Tooltip("기상 트리거 이름")]
|
||||
[SerializeField] private string recoverTriggerParam = "Recover";
|
||||
|
||||
[Header("Settings")]
|
||||
[Tooltip("애니메이션 파라미터가 없을 때 경고 로그 출력")]
|
||||
[SerializeField] private bool logMissingAnimationParams = false;
|
||||
|
||||
private readonly NetworkVariable<bool> isDowned = new NetworkVariable<bool>(false);
|
||||
private float downRemainingTime;
|
||||
|
||||
/// <summary>
|
||||
/// 다운 상태 여부
|
||||
/// </summary>
|
||||
public bool IsDowned => isDowned.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 넉백 강제 이동 진행 여부
|
||||
/// </summary>
|
||||
public bool IsKnockbackActive => playerMovement != null && playerMovement.IsForcedMoving;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
ResolveReferences();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsServer || !isDowned.Value)
|
||||
return;
|
||||
|
||||
downRemainingTime -= Time.deltaTime;
|
||||
if (downRemainingTime <= 0f)
|
||||
{
|
||||
RecoverFromDown();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 넉백을 적용합니다.
|
||||
/// </summary>
|
||||
public void ApplyKnockback(Vector3 worldVelocity, float duration, bool playHitAnimation = true)
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
ResolveReferences();
|
||||
|
||||
if (networkController != null && networkController.IsDead)
|
||||
return;
|
||||
|
||||
playerMovement?.ApplyForcedMovement(worldVelocity, duration);
|
||||
|
||||
if (playHitAnimation)
|
||||
{
|
||||
TriggerAnimationRpc(hitTriggerParam);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다운 상태를 적용합니다.
|
||||
/// </summary>
|
||||
public void ApplyDown(float duration)
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
ResolveReferences();
|
||||
|
||||
if (networkController != null && networkController.IsDead)
|
||||
return;
|
||||
|
||||
downRemainingTime = Mathf.Max(downRemainingTime, duration);
|
||||
|
||||
if (isDowned.Value)
|
||||
return;
|
||||
|
||||
isDowned.Value = true;
|
||||
skillController?.CancelSkill(SkillCancelReason.HitReaction);
|
||||
playerMovement?.ClearForcedMovement();
|
||||
TriggerAnimationRpc(downTriggerParam);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다운 상태를 해제합니다.
|
||||
/// </summary>
|
||||
public void RecoverFromDown()
|
||||
{
|
||||
if (!IsServer || !isDowned.Value)
|
||||
return;
|
||||
|
||||
isDowned.Value = false;
|
||||
downRemainingTime = 0f;
|
||||
TriggerAnimationRpc(recoverTriggerParam);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 피격 상태를 즉시 초기화합니다.
|
||||
/// </summary>
|
||||
public void ClearHitReactionState(bool playRecoverAnimation = false)
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
ResolveReferences();
|
||||
|
||||
playerMovement?.ClearForcedMovement();
|
||||
|
||||
if (!isDowned.Value)
|
||||
return;
|
||||
|
||||
isDowned.Value = false;
|
||||
downRemainingTime = 0f;
|
||||
|
||||
if (playRecoverAnimation)
|
||||
{
|
||||
TriggerAnimationRpc(recoverTriggerParam);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Everyone)]
|
||||
private void TriggerAnimationRpc(string triggerName)
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (animator == null || string.IsNullOrWhiteSpace(triggerName))
|
||||
return;
|
||||
|
||||
if (!HasTrigger(triggerName))
|
||||
{
|
||||
if (logMissingAnimationParams)
|
||||
{
|
||||
Debug.LogWarning($"[HitReaction] Animator trigger not found: {triggerName}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
animator.SetTrigger(triggerName);
|
||||
}
|
||||
|
||||
private bool HasTrigger(string triggerName)
|
||||
{
|
||||
if (animator == null || string.IsNullOrWhiteSpace(triggerName))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < animator.parameterCount; i++)
|
||||
{
|
||||
AnimatorControllerParameter parameter = animator.GetParameter(i);
|
||||
if (parameter.type == AnimatorControllerParameterType.Trigger && parameter.name == triggerName)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (playerMovement == null)
|
||||
playerMovement = GetComponent<PlayerMovement>();
|
||||
|
||||
if (skillController == null)
|
||||
skillController = GetComponent<SkillController>();
|
||||
|
||||
if (networkController == null)
|
||||
networkController = GetComponent<PlayerNetworkController>();
|
||||
|
||||
if (animator == null)
|
||||
animator = GetComponentInChildren<Animator>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ebad07a2d5fc29b4ba061866bfa1568e
|
||||
@@ -93,7 +93,7 @@ namespace Colosseum.Player
|
||||
GUILayout.Label($"사망 상태: {(networkController != null && networkController.IsDead ? "Dead" : "Alive")}");
|
||||
if (TryGetComponent<PlayerActionState>(out var actionState))
|
||||
{
|
||||
GUILayout.Label($"무적 상태: {(actionState.IsDamageImmune ? "Immune" : "Normal")} / 이동:{actionState.CanMove} / 스킬:{actionState.CanUseSkills}");
|
||||
GUILayout.Label($"무적:{(actionState.IsDamageImmune ? "On" : "Off")} / 다운:{(actionState.IsDowned ? "On" : "Off")} / 이동:{actionState.CanMove} / 스킬:{actionState.CanUseSkills}");
|
||||
}
|
||||
GUILayout.Label("입력 예시: 기절 / Data_Abnormality_Player_Stun / 0");
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace Colosseum.Player
|
||||
[SerializeField] private PlayerNetworkController networkController;
|
||||
[SerializeField] private PlayerSkillInput skillInput;
|
||||
[SerializeField] private SkillController skillController;
|
||||
[SerializeField] private PlayerMovement playerMovement;
|
||||
[SerializeField] private HitReactionController hitReactionController;
|
||||
|
||||
[Header("Test Data")]
|
||||
[SerializeField] private AbnormalityData stunData;
|
||||
@@ -84,7 +86,7 @@ namespace Colosseum.Player
|
||||
ResolveReferences();
|
||||
LoadDefaultAssetsIfNeeded();
|
||||
|
||||
if (abnormalityManager == null || actionState == null || networkController == null || stunData == null || silenceData == null)
|
||||
if (abnormalityManager == null || actionState == null || networkController == null || playerMovement == null || hitReactionController == null || stunData == null || silenceData == null)
|
||||
{
|
||||
Debug.LogWarning("[AbnormalityVerification] Missing references or test data.");
|
||||
yield break;
|
||||
@@ -105,9 +107,16 @@ namespace Colosseum.Player
|
||||
Verify("초기 상태: 이동 가능", actionState.CanMove);
|
||||
Verify("초기 상태: 스킬 사용 가능", actionState.CanUseSkills);
|
||||
Verify("초기 상태: 무적 상태 아님", !actionState.IsDamageImmune);
|
||||
Verify("초기 상태: 마지막 취소 이유 없음", skillController == null || skillController.LastCancelReason == SkillCancelReason.None);
|
||||
|
||||
yield return RunInvincibilitySkillVerification();
|
||||
|
||||
yield return RunStunCancellationVerification();
|
||||
|
||||
yield return RunDownVerification();
|
||||
|
||||
yield return RunKnockbackVerification();
|
||||
|
||||
abnormalityManager.ApplyAbnormality(stunData, gameObject);
|
||||
yield return new WaitForSeconds(settleDelay);
|
||||
|
||||
@@ -187,6 +196,10 @@ namespace Colosseum.Player
|
||||
skillInput = GetComponent<PlayerSkillInput>();
|
||||
if (skillController == null)
|
||||
skillController = GetComponent<SkillController>();
|
||||
if (playerMovement == null)
|
||||
playerMovement = GetComponent<PlayerMovement>();
|
||||
if (hitReactionController == null)
|
||||
hitReactionController = GetComponent<HitReactionController>();
|
||||
}
|
||||
|
||||
private void LoadDefaultAssetsIfNeeded()
|
||||
@@ -276,6 +289,109 @@ namespace Colosseum.Player
|
||||
Verify("무적 해제: IsDamageImmune false", !actionState.IsDamageImmune);
|
||||
}
|
||||
|
||||
private IEnumerator RunStunCancellationVerification()
|
||||
{
|
||||
SkillData cancellableSkill = FindCancellableSkill();
|
||||
if (cancellableSkill == null)
|
||||
{
|
||||
AppendLine("[SKIP] 기절 강제 취소 검증: 테스트용 스킬이 없습니다.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (skillController != null)
|
||||
{
|
||||
yield return WaitForConditionOrTimeout(() => !skillController.IsPlayingAnimation, 1.5f);
|
||||
}
|
||||
|
||||
if (skillController == null || !skillController.ExecuteSkill(cancellableSkill))
|
||||
{
|
||||
Verify("기절 강제 취소 검증: 스킬 실행 성공", false);
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
|
||||
abnormalityManager.ApplyAbnormality(stunData, gameObject);
|
||||
yield return WaitForConditionOrTimeout(() => abnormalityManager.IsStunned, settleDelay + 0.5f);
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
|
||||
Verify("기절 강제 취소: 스킬 애니메이션 중단", !skillController.IsPlayingAnimation);
|
||||
Verify("기절 강제 취소: 취소 이유 기록", skillController.LastCancelReason == SkillCancelReason.Stun);
|
||||
|
||||
yield return new WaitForSeconds(stunData.duration + settleDelay);
|
||||
}
|
||||
|
||||
private IEnumerator RunKnockbackVerification()
|
||||
{
|
||||
abnormalityManager.RemoveAllAbnormalities();
|
||||
hitReactionController.ClearHitReactionState();
|
||||
yield return new WaitForSeconds(settleDelay);
|
||||
|
||||
Vector3 startPosition = transform.position;
|
||||
Vector3 knockbackVelocity = Vector3.back * 6f;
|
||||
|
||||
RequestKnockbackRpc(knockbackVelocity, 0.2f);
|
||||
yield return new WaitForSeconds(0.3f);
|
||||
|
||||
float movedDistance = Vector3.Distance(startPosition, transform.position);
|
||||
Verify("넉백 적용: 위치 이동 발생", movedDistance > 0.2f);
|
||||
Verify("넉백 적용: 강제 이동 종료", !playerMovement.IsForcedMoving);
|
||||
}
|
||||
|
||||
private IEnumerator RunDownVerification()
|
||||
{
|
||||
SkillData cancellableSkill = FindCancellableSkill();
|
||||
if (cancellableSkill == null)
|
||||
{
|
||||
AppendLine("[SKIP] 다운 검증: 테스트용 스킬이 없습니다.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (skillController != null)
|
||||
{
|
||||
yield return WaitForConditionOrTimeout(() => !skillController.IsPlayingAnimation, 1.5f);
|
||||
}
|
||||
|
||||
if (skillController == null || !skillController.ExecuteSkill(cancellableSkill))
|
||||
{
|
||||
Verify("다운 강제 취소 검증: 스킬 실행 성공", false);
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
|
||||
RequestDownRpc(0.6f);
|
||||
yield return WaitForConditionOrTimeout(() => hitReactionController.IsDowned, settleDelay + 0.5f);
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
|
||||
Verify("다운 적용: IsDowned", hitReactionController.IsDowned);
|
||||
Verify("다운 적용: ActionState.IsDowned", actionState.IsDowned);
|
||||
Verify("다운 강제 취소: 스킬 애니메이션 중단", !skillController.IsPlayingAnimation);
|
||||
Verify("다운 강제 취소: 취소 이유 기록", skillController.LastCancelReason == SkillCancelReason.HitReaction);
|
||||
Verify("다운 적용: 이동 불가", !actionState.CanMove);
|
||||
Verify("다운 적용: 점프 불가", !actionState.CanJump);
|
||||
Verify("다운 적용: 스킬 사용 불가", !actionState.CanUseSkills);
|
||||
Verify("다운 적용: 이동속도 0", Mathf.Approximately(actionState.MoveSpeedMultiplier, 0f));
|
||||
|
||||
yield return WaitForConditionOrTimeout(() => !hitReactionController.IsDowned, 1.5f);
|
||||
|
||||
Verify("다운 해제: IsDowned false", !hitReactionController.IsDowned);
|
||||
Verify("다운 해제: 이동 가능 복구", actionState.CanMove);
|
||||
Verify("다운 해제: 스킬 사용 가능 복구", actionState.CanUseSkills);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server)]
|
||||
private void RequestDownRpc(float duration)
|
||||
{
|
||||
hitReactionController?.ApplyDown(duration);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server)]
|
||||
private void RequestKnockbackRpc(Vector3 velocity, float duration)
|
||||
{
|
||||
hitReactionController?.ApplyKnockback(velocity, duration, false);
|
||||
}
|
||||
|
||||
private float GetSkillDuration(SkillData skill)
|
||||
{
|
||||
if (skill == null || skill.SkillClip == null)
|
||||
@@ -284,6 +400,21 @@ namespace Colosseum.Player
|
||||
return Mathf.Max(settleDelay, skill.SkillClip.length / Mathf.Max(0.1f, skill.AnimationSpeed));
|
||||
}
|
||||
|
||||
private SkillData FindCancellableSkill()
|
||||
{
|
||||
if (skillInput == null)
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
SkillData skill = skillInput.GetSkill(i);
|
||||
if (skill != null)
|
||||
return skill;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IEnumerator WaitForConditionOrTimeout(Func<bool> predicate, float timeout)
|
||||
{
|
||||
float elapsed = 0f;
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace Colosseum.Player
|
||||
[Tooltip("스킬 실행 관리자")]
|
||||
[SerializeField] private SkillController skillController;
|
||||
|
||||
[Tooltip("피격 제어 관리자")]
|
||||
[SerializeField] private HitReactionController hitReactionController;
|
||||
|
||||
[Tooltip("관전 관리자")]
|
||||
[SerializeField] private PlayerSpectator spectator;
|
||||
|
||||
@@ -35,6 +38,11 @@ namespace Colosseum.Player
|
||||
/// </summary>
|
||||
public bool IsStunned => abnormalityManager != null && abnormalityManager.IsStunned;
|
||||
|
||||
/// <summary>
|
||||
/// 다운 상태 여부
|
||||
/// </summary>
|
||||
public bool IsDowned => hitReactionController != null && hitReactionController.IsDowned;
|
||||
|
||||
/// <summary>
|
||||
/// 침묵 상태 여부
|
||||
/// </summary>
|
||||
@@ -68,17 +76,17 @@ namespace Colosseum.Player
|
||||
/// <summary>
|
||||
/// 플레이어가 직접 이동 입력을 사용할 수 있는지 여부
|
||||
/// </summary>
|
||||
public bool CanMove => CanReceiveInput && !IsStunned && !BlocksMovementForCurrentSkill();
|
||||
public bool CanMove => CanReceiveInput && !IsStunned && !IsDowned && !BlocksMovementForCurrentSkill();
|
||||
|
||||
/// <summary>
|
||||
/// 점프 가능 여부
|
||||
/// </summary>
|
||||
public bool CanJump => CanReceiveInput && !IsStunned && !BlocksJumpForCurrentSkill();
|
||||
public bool CanJump => CanReceiveInput && !IsStunned && !IsDowned && !BlocksJumpForCurrentSkill();
|
||||
|
||||
/// <summary>
|
||||
/// 일반 스킬 시작 가능 여부
|
||||
/// </summary>
|
||||
public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsSilenced && !BlocksSkillUseForCurrentSkill();
|
||||
public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsDowned && !IsSilenced && !BlocksSkillUseForCurrentSkill();
|
||||
|
||||
/// <summary>
|
||||
/// 특정 스킬의 시작 가능 여부.
|
||||
@@ -89,7 +97,7 @@ namespace Colosseum.Player
|
||||
if (skill == null)
|
||||
return false;
|
||||
|
||||
if (!CanReceiveInput || IsStunned || IsSilenced)
|
||||
if (!CanReceiveInput || IsStunned || IsDowned || IsSilenced)
|
||||
return false;
|
||||
|
||||
return !BlocksSkillUseForCurrentSkill();
|
||||
@@ -102,7 +110,7 @@ namespace Colosseum.Player
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!CanReceiveInput || IsStunned)
|
||||
if (!CanReceiveInput || IsStunned || IsDowned)
|
||||
return 0f;
|
||||
|
||||
return abnormalityManager != null ? abnormalityManager.MoveSpeedMultiplier : 1f;
|
||||
@@ -117,6 +125,8 @@ namespace Colosseum.Player
|
||||
abnormalityManager = GetComponent<AbnormalityManager>();
|
||||
if (skillController == null)
|
||||
skillController = GetComponent<SkillController>();
|
||||
if (hitReactionController == null)
|
||||
hitReactionController = GetOrCreateHitReactionController();
|
||||
if (spectator == null)
|
||||
spectator = GetComponentInChildren<PlayerSpectator>();
|
||||
}
|
||||
@@ -144,5 +154,14 @@ namespace Colosseum.Player
|
||||
|
||||
return CurrentSkill == null || CurrentSkill.BlockOtherSkillsWhileCasting;
|
||||
}
|
||||
|
||||
private HitReactionController GetOrCreateHitReactionController()
|
||||
{
|
||||
HitReactionController foundController = GetComponent<HitReactionController>();
|
||||
if (foundController != null)
|
||||
return foundController;
|
||||
|
||||
return gameObject.AddComponent<HitReactionController>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace Colosseum.Player
|
||||
private InputSystem_Actions inputActions;
|
||||
private bool isJumping;
|
||||
private bool wasGrounded;
|
||||
private Vector3 forcedMovementVelocity;
|
||||
private float forcedMovementRemaining;
|
||||
|
||||
// 클라이언트가 기록, 서버가 소비하는 월드 스페이스 이동 방향
|
||||
private NetworkVariable<Vector2> netMoveInput = new NetworkVariable<Vector2>(
|
||||
@@ -48,6 +50,7 @@ namespace Colosseum.Player
|
||||
public float CurrentMoveSpeed => netMoveInput.Value.magnitude * moveSpeed * GetMoveSpeedMultiplier();
|
||||
public bool IsGrounded => characterController != null ? characterController.isGrounded : false;
|
||||
public bool IsJumping => isJumping;
|
||||
public bool IsForcedMoving => forcedMovementRemaining > 0f && forcedMovementVelocity.sqrMagnitude > 0.0001f;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
@@ -115,6 +118,7 @@ namespace Colosseum.Player
|
||||
{
|
||||
CleanupInputActions();
|
||||
moveInput = Vector2.zero;
|
||||
ClearForcedMovement();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -143,6 +147,7 @@ namespace Colosseum.Player
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
CleanupInputActions();
|
||||
ClearForcedMovement();
|
||||
}
|
||||
|
||||
private void OnMovePerformed(InputAction.CallbackContext context) => moveInput = context.ReadValue<Vector2>();
|
||||
@@ -259,10 +264,12 @@ namespace Colosseum.Player
|
||||
{
|
||||
if (characterController == null) return;
|
||||
|
||||
Vector3 forcedDelta = ConsumeForcedMovementDelta(Time.deltaTime);
|
||||
|
||||
if (skillController != null && skillController.IsPlayingAnimation)
|
||||
{
|
||||
if (!skillController.UsesRootMotion)
|
||||
characterController.Move(velocity * Time.deltaTime);
|
||||
characterController.Move(velocity * Time.deltaTime + forcedDelta);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -278,7 +285,7 @@ namespace Colosseum.Player
|
||||
moveDirection = Vector3.zero;
|
||||
|
||||
float actualMoveSpeed = moveSpeed * GetMoveSpeedMultiplier();
|
||||
characterController.Move((moveDirection * actualMoveSpeed + velocity) * Time.deltaTime);
|
||||
characterController.Move((moveDirection * actualMoveSpeed + velocity) * Time.deltaTime + forcedDelta);
|
||||
|
||||
if (moveDirection != Vector3.zero)
|
||||
{
|
||||
@@ -323,6 +330,33 @@ namespace Colosseum.Player
|
||||
return actionState.MoveSpeedMultiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 넉백처럼 입력과 무관한 강제 이동을 적용합니다.
|
||||
/// </summary>
|
||||
public void ApplyForcedMovement(Vector3 worldVelocity, float duration)
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
if (duration <= 0f || worldVelocity.sqrMagnitude <= 0.0001f)
|
||||
{
|
||||
ClearForcedMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
forcedMovementVelocity = worldVelocity;
|
||||
forcedMovementRemaining = duration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 강제 이동 상태를 즉시 초기화합니다.
|
||||
/// </summary>
|
||||
public void ClearForcedMovement()
|
||||
{
|
||||
forcedMovementVelocity = Vector3.zero;
|
||||
forcedMovementRemaining = 0f;
|
||||
}
|
||||
|
||||
private PlayerActionState GetOrCreateActionState()
|
||||
{
|
||||
var foundState = GetComponent<PlayerActionState>();
|
||||
@@ -343,6 +377,7 @@ namespace Colosseum.Player
|
||||
if (!skillController.UsesRootMotion) return;
|
||||
|
||||
Vector3 deltaPosition = animator.deltaPosition;
|
||||
Vector3 forcedDelta = ConsumeForcedMovementDelta(Time.deltaTime);
|
||||
|
||||
if (blockedDirection != Vector3.zero)
|
||||
{
|
||||
@@ -357,11 +392,11 @@ namespace Colosseum.Player
|
||||
if (skillController.IgnoreRootMotionY)
|
||||
{
|
||||
deltaPosition.y = 0f;
|
||||
characterController.Move(deltaPosition + velocity * Time.deltaTime);
|
||||
characterController.Move(deltaPosition + velocity * Time.deltaTime + forcedDelta);
|
||||
}
|
||||
else
|
||||
{
|
||||
characterController.Move(deltaPosition);
|
||||
characterController.Move(deltaPosition + forcedDelta);
|
||||
}
|
||||
|
||||
if (animator.deltaRotation != Quaternion.identity)
|
||||
@@ -372,5 +407,22 @@ namespace Colosseum.Player
|
||||
|
||||
wasGrounded = characterController.isGrounded;
|
||||
}
|
||||
|
||||
private Vector3 ConsumeForcedMovementDelta(float deltaTime)
|
||||
{
|
||||
if (forcedMovementRemaining <= 0f || forcedMovementVelocity.sqrMagnitude <= 0.0001f)
|
||||
return Vector3.zero;
|
||||
|
||||
float appliedDeltaTime = Mathf.Min(deltaTime, forcedMovementRemaining);
|
||||
forcedMovementRemaining = Mathf.Max(0f, forcedMovementRemaining - appliedDeltaTime);
|
||||
|
||||
Vector3 delta = forcedMovementVelocity * appliedDeltaTime;
|
||||
if (forcedMovementRemaining <= 0f)
|
||||
{
|
||||
forcedMovementVelocity = Vector3.zero;
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,9 +176,16 @@ namespace Colosseum.Player
|
||||
var movement = GetComponent<PlayerMovement>();
|
||||
if (movement != null)
|
||||
{
|
||||
movement.ClearForcedMovement();
|
||||
movement.enabled = false;
|
||||
}
|
||||
|
||||
var hitReactionController = GetComponent<HitReactionController>();
|
||||
if (hitReactionController != null)
|
||||
{
|
||||
hitReactionController.ClearHitReactionState();
|
||||
}
|
||||
|
||||
// 스킬 입력 비활성화
|
||||
var skillInput = GetComponent<PlayerSkillInput>();
|
||||
if (skillInput != null)
|
||||
@@ -190,7 +197,7 @@ namespace Colosseum.Player
|
||||
var skillController = GetComponent<SkillController>();
|
||||
if (skillController != null)
|
||||
{
|
||||
skillController.CancelSkill();
|
||||
skillController.CancelSkill(SkillCancelReason.Death);
|
||||
}
|
||||
|
||||
// 모든 클라이언트에서 사망 애니메이션 재생
|
||||
@@ -222,9 +229,16 @@ namespace Colosseum.Player
|
||||
var movement = GetComponent<PlayerMovement>();
|
||||
if (movement != null)
|
||||
{
|
||||
movement.ClearForcedMovement();
|
||||
movement.enabled = true;
|
||||
}
|
||||
|
||||
var hitReactionController = GetComponent<HitReactionController>();
|
||||
if (hitReactionController != null)
|
||||
{
|
||||
hitReactionController.ClearHitReactionState();
|
||||
}
|
||||
|
||||
// 스킬 입력 재활성화
|
||||
var skillInput = GetComponent<PlayerSkillInput>();
|
||||
if (skillInput != null)
|
||||
@@ -239,6 +253,12 @@ namespace Colosseum.Player
|
||||
animator.Rebind();
|
||||
}
|
||||
|
||||
var skillController = GetComponent<SkillController>();
|
||||
if (skillController != null)
|
||||
{
|
||||
skillController.CancelSkill(SkillCancelReason.Respawn);
|
||||
}
|
||||
|
||||
OnRespawned?.Invoke(this);
|
||||
|
||||
Debug.Log($"[Player] Player {OwnerClientId} respawned!");
|
||||
|
||||
42
Assets/_Game/Scripts/Skills/Effects/DownEffect.cs
Normal file
42
Assets/_Game/Scripts/Skills/Effects/DownEffect.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Player;
|
||||
|
||||
namespace Colosseum.Skills.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// 대상에게 다운 상태를 적용하는 스킬 효과입니다.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "DownEffect", menuName = "Colosseum/Skills/Effects/Down")]
|
||||
public class DownEffect : SkillEffect
|
||||
{
|
||||
[Header("Settings")]
|
||||
[Tooltip("다운 지속 시간")]
|
||||
[Min(0f)] [SerializeField] private float duration = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// 다운 지속 시간
|
||||
/// </summary>
|
||||
public float Duration => duration;
|
||||
|
||||
/// <summary>
|
||||
/// 다운 효과를 적용합니다.
|
||||
/// </summary>
|
||||
protected override void ApplyEffect(GameObject source, GameObject target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
Debug.LogWarning("[DownEffect] Target is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.TryGetComponent(out HitReactionController hitReactionController))
|
||||
{
|
||||
Debug.LogWarning($"[DownEffect] HitReactionController not found on target: {target.name}");
|
||||
return;
|
||||
}
|
||||
|
||||
hitReactionController.ApplyDown(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Skills/Effects/DownEffect.cs.meta
Normal file
2
Assets/_Game/Scripts/Skills/Effects/DownEffect.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41c96b54a96cdb84c9bda774775b0a1a
|
||||
@@ -1,5 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Player;
|
||||
|
||||
namespace Colosseum.Skills.Effects
|
||||
{
|
||||
/// <summary>
|
||||
@@ -11,6 +13,7 @@ namespace Colosseum.Skills.Effects
|
||||
[Header("Knockback Settings")]
|
||||
[Min(0f)] [SerializeField] private float force = 5f;
|
||||
[SerializeField] private float upwardForce = 2f;
|
||||
[Min(0.05f)] [SerializeField] private float duration = 0.2f;
|
||||
|
||||
protected override void ApplyEffect(GameObject caster, GameObject target)
|
||||
{
|
||||
@@ -18,13 +21,23 @@ namespace Colosseum.Skills.Effects
|
||||
|
||||
Vector3 direction = target.transform.position - caster.transform.position;
|
||||
direction.y = 0f;
|
||||
direction.Normalize();
|
||||
if (direction.sqrMagnitude <= 0.0001f)
|
||||
direction = caster.transform.forward;
|
||||
else
|
||||
direction.Normalize();
|
||||
|
||||
Vector3 knockback = direction * force + Vector3.up * upwardForce;
|
||||
Vector3 knockbackVelocity = direction * force + Vector3.up * upwardForce;
|
||||
|
||||
// TODO: 실제 물리 시스템 연동
|
||||
// if (target.TryGetComponent<Rigidbody>(out var rb))
|
||||
// rb.AddForce(knockback, ForceMode.Impulse);
|
||||
if (target.TryGetComponent<HitReactionController>(out var hitReactionController))
|
||||
{
|
||||
hitReactionController.ApplyKnockback(knockbackVelocity, duration);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.TryGetComponent<PlayerMovement>(out var playerMovement))
|
||||
{
|
||||
playerMovement.ApplyForcedMovement(knockbackVelocity, duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,19 @@ using Unity.Netcode;
|
||||
|
||||
namespace Colosseum.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// 스킬 강제 취소 이유
|
||||
/// </summary>
|
||||
public enum SkillCancelReason
|
||||
{
|
||||
None,
|
||||
Manual,
|
||||
Death,
|
||||
Stun,
|
||||
HitReaction,
|
||||
Respawn,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 실행을 관리하는 컴포넌트.
|
||||
/// 애니메이션 이벤트 기반으로 효과가 발동됩니다.
|
||||
@@ -31,6 +44,12 @@ namespace Colosseum.Skills
|
||||
[Tooltip("범위 표시 지속 시간")]
|
||||
[Min(0.1f)] [SerializeField] private float debugDrawDuration = 1f;
|
||||
|
||||
[Header("디버그")]
|
||||
[Tooltip("마지막으로 강제 취소된 스킬 이름")]
|
||||
[SerializeField] private string lastCancelledSkillName = string.Empty;
|
||||
[Tooltip("마지막 강제 취소 이유")]
|
||||
[SerializeField] private SkillCancelReason lastCancelReason = SkillCancelReason.None;
|
||||
|
||||
// 현재 실행 중인 스킬
|
||||
private SkillData currentSkill;
|
||||
private bool skillEndRequested; // OnSkillEnd 이벤트 호출 여부
|
||||
@@ -47,9 +66,14 @@ namespace Colosseum.Skills
|
||||
public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY;
|
||||
public SkillData CurrentSkill => currentSkill;
|
||||
public Animator Animator => animator;
|
||||
public SkillCancelReason LastCancelReason => lastCancelReason;
|
||||
public string LastCancelledSkillName => lastCancelledSkillName;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
lastCancelledSkillName = string.Empty;
|
||||
lastCancelReason = SkillCancelReason.None;
|
||||
|
||||
if (animator == null)
|
||||
{
|
||||
animator = GetComponentInChildren<Animator>();
|
||||
@@ -135,6 +159,7 @@ namespace Colosseum.Skills
|
||||
currentSkill = skill;
|
||||
skillEndRequested = false;
|
||||
waitingForEndAnimation = false;
|
||||
lastCancelReason = SkillCancelReason.None;
|
||||
|
||||
if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}");
|
||||
|
||||
@@ -339,16 +364,24 @@ namespace Colosseum.Skills
|
||||
if (debugMode) Debug.Log($"[Skill] End requested: {currentSkill.SkillName} (will complete after animation)");
|
||||
}
|
||||
|
||||
public void CancelSkill()
|
||||
/// <summary>
|
||||
/// 현재 스킬을 강제 취소합니다.
|
||||
/// </summary>
|
||||
public bool CancelSkill(SkillCancelReason reason = SkillCancelReason.Manual)
|
||||
{
|
||||
if (currentSkill != null)
|
||||
{
|
||||
if (debugMode) Debug.Log($"Skill cancelled: {currentSkill.SkillName}");
|
||||
RestoreBaseController();
|
||||
currentSkill = null;
|
||||
skillEndRequested = false;
|
||||
waitingForEndAnimation = false;
|
||||
}
|
||||
if (currentSkill == null)
|
||||
return false;
|
||||
|
||||
lastCancelledSkillName = currentSkill.SkillName;
|
||||
lastCancelReason = reason;
|
||||
|
||||
Debug.Log($"[Skill] Cancelled: {currentSkill.SkillName} / reason={reason}");
|
||||
|
||||
RestoreBaseController();
|
||||
currentSkill = null;
|
||||
skillEndRequested = false;
|
||||
waitingForEndAnimation = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsOnCooldown(SkillData skill)
|
||||
|
||||
Reference in New Issue
Block a user