diff --git a/Assets/_Game/Prefabs/Bosses/Prefab_Boss_TestBoss.prefab b/Assets/_Game/Prefabs/Bosses/Prefab_Boss_TestBoss.prefab index 11ecbb02..9fcf0b7a 100644 --- a/Assets/_Game/Prefabs/Bosses/Prefab_Boss_TestBoss.prefab +++ b/Assets/_Game/Prefabs/Bosses/Prefab_Boss_TestBoss.prefab @@ -9,7 +9,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6643653656805266137} - m_Layer: 0 + m_Layer: 6 m_Name: IndexFinger_03 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -41,7 +41,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 872883229803693180} - m_Layer: 0 + m_Layer: 6 m_Name: Root m_TagString: Untagged m_Icon: {fileID: 0} @@ -73,7 +73,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 1121004277287172730} - m_Layer: 0 + m_Layer: 6 m_Name: Ankle_L m_TagString: Untagged m_Icon: {fileID: 0} @@ -106,7 +106,7 @@ GameObject: m_Component: - component: {fileID: 8242298026093095983} - component: {fileID: 7340647355570253925} - m_Layer: 0 + m_Layer: 6 m_Name: Character_BarbarianGiant_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -246,7 +246,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 144812691924856346} - m_Layer: 0 + m_Layer: 6 m_Name: Toes_L m_TagString: Untagged m_Icon: {fileID: 0} @@ -277,7 +277,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6189945741236030909} - m_Layer: 0 + m_Layer: 6 m_Name: UpperLeg_L m_TagString: Untagged m_Icon: {fileID: 0} @@ -310,7 +310,7 @@ GameObject: m_Component: - component: {fileID: 5234165385643310874} - component: {fileID: 4196026135510989304} - m_Layer: 0 + m_Layer: 6 m_Name: Character_Dwarf_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -450,7 +450,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 4953413866640159768} - m_Layer: 0 + m_Layer: 6 m_Name: Eyebrows m_TagString: Untagged m_Icon: {fileID: 0} @@ -481,7 +481,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6305222539219953510} - m_Layer: 0 + m_Layer: 6 m_Name: IndexFinger_01 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -513,7 +513,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6261344531192022962} - m_Layer: 0 + m_Layer: 6 m_Name: Head m_TagString: Untagged m_Icon: {fileID: 0} @@ -547,7 +547,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 1156866845888020345} - m_Layer: 0 + m_Layer: 6 m_Name: Helmet m_TagString: Untagged m_Icon: {fileID: 0} @@ -579,7 +579,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6876742027977645837} - m_Layer: 0 + m_Layer: 6 m_Name: Ball_L m_TagString: Untagged m_Icon: {fileID: 0} @@ -612,7 +612,7 @@ GameObject: m_Component: - component: {fileID: 9024640953938921730} - component: {fileID: 4040563405028547472} - m_Layer: 0 + m_Layer: 6 m_Name: Character_Slayer_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -752,7 +752,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 7074235824420974134} - m_Layer: 0 + m_Layer: 6 m_Name: Finger_04 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -783,7 +783,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6961594731814923999} - m_Layer: 0 + m_Layer: 6 m_Name: IndexFinger_03 m_TagString: Untagged m_Icon: {fileID: 0} @@ -815,7 +815,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 2982108666446807220} - m_Layer: 0 + m_Layer: 6 m_Name: Finger_04 m_TagString: Untagged m_Icon: {fileID: 0} @@ -846,7 +846,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 1902043979864175257} - m_Layer: 0 + m_Layer: 6 m_Name: Toes_R m_TagString: Untagged m_Icon: {fileID: 0} @@ -877,7 +877,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 2670510357683655273} - m_Layer: 0 + m_Layer: 6 m_Name: Spine_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -909,7 +909,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6480956261595266767} - m_Layer: 0 + m_Layer: 6 m_Name: Clavicle_L m_TagString: Untagged m_Icon: {fileID: 0} @@ -941,7 +941,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 7643421511305802608} - m_Layer: 0 + m_Layer: 6 m_Name: Neck m_TagString: Untagged m_Icon: {fileID: 0} @@ -973,7 +973,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 132307383957219980} - m_Layer: 0 + m_Layer: 6 m_Name: Ankle_R m_TagString: Untagged m_Icon: {fileID: 0} @@ -1005,7 +1005,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6718570607748687175} - m_Layer: 0 + m_Layer: 6 m_Name: Elbow_L m_TagString: Untagged m_Icon: {fileID: 0} @@ -1038,7 +1038,7 @@ GameObject: m_Component: - component: {fileID: 2941051679179816350} - component: {fileID: 1049515515378192443} - m_Layer: 0 + m_Layer: 6 m_Name: Character_Pig_Butcher_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1178,7 +1178,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 2274080434550477928} - m_Layer: 0 + m_Layer: 6 m_Name: Thumb_02 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1210,7 +1210,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 3224253035183249078} - m_Layer: 0 + m_Layer: 6 m_Name: Finger_03 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1242,7 +1242,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 742209535846353542} - m_Layer: 0 + m_Layer: 6 m_Name: Eyes m_TagString: Untagged m_Icon: {fileID: 0} @@ -1273,7 +1273,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6022304106011789127} - m_Layer: 0 + m_Layer: 6 m_Name: LowerLeg_L m_TagString: Untagged m_Icon: {fileID: 0} @@ -1305,7 +1305,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 985120257555972102} - m_Layer: 0 + m_Layer: 6 m_Name: IndexFinger_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1337,7 +1337,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 2158936302564180189} - m_Layer: 0 + m_Layer: 6 m_Name: IndexFinger_04 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1369,7 +1369,7 @@ GameObject: m_Component: - component: {fileID: 3702107543611294815} - component: {fileID: 752898117895215478} - m_Layer: 0 + m_Layer: 6 m_Name: Character_ElementalGolem_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1509,7 +1509,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 9126929411809314898} - m_Layer: 0 + m_Layer: 6 m_Name: IndexFinger_02 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1542,7 +1542,7 @@ GameObject: m_Component: - component: {fileID: 2792811216651560850} - component: {fileID: 556967472877552751} - m_Layer: 0 + m_Layer: 6 m_Name: Character_Troll_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1683,7 +1683,7 @@ GameObject: m_Component: - component: {fileID: 3983418530701611865} - component: {fileID: 8013341123487295681} - m_Layer: 0 + m_Layer: 6 m_Name: Character_Big_Ork_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1823,7 +1823,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 8039592807724159502} - m_Layer: 0 + m_Layer: 6 m_Name: Thumb_03 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1854,7 +1854,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 1126755880182120861} - m_Layer: 0 + m_Layer: 6 m_Name: Elbow_R m_TagString: Untagged m_Icon: {fileID: 0} @@ -1886,7 +1886,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 7553931185384545452} - m_Layer: 0 + m_Layer: 6 m_Name: Spine_03 m_TagString: Untagged m_Icon: {fileID: 0} @@ -1930,7 +1930,7 @@ GameObject: - component: {fileID: 8818883032728065057} - component: {fileID: -2857689419101920665} - component: {fileID: 7544406269366897481} - m_Layer: 0 + m_Layer: 6 m_Name: Prefab_Boss_TestBoss m_TagString: Untagged m_Icon: {fileID: 0} @@ -1998,7 +1998,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} m_Name: m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject - GlobalObjectIdHash: 3958201435 + GlobalObjectIdHash: 860882280 InScenePlacedSourceGlobalObjectIdHash: 223369646 DeferredDespawnTick: 0 Ownership: 1 @@ -2134,7 +2134,7 @@ MonoBehaviour: m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillController animator: {fileID: 4019041888965840580} baseController: {fileID: 9100000, guid: 4bd980f1a222c5b468136f7e717925d5, type: 2} - baseSkillClip: {fileID: -7717634560727564301, guid: 0f6fd9302e489b94d96774e2713b1317, type: 3} + baseSkillClip: {fileID: -7717634560727564301, guid: 4005a77aa7d531742b1de1bec27001b1, type: 3} debugMode: 1 showAreaDebug: 1 debugDrawDuration: 1 @@ -2175,7 +2175,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6095322058662237816} - m_Layer: 0 + m_Layer: 6 m_Name: Hand_R m_TagString: Untagged m_Icon: {fileID: 0} @@ -2210,7 +2210,7 @@ GameObject: m_Component: - component: {fileID: 8771294253604273166} - component: {fileID: 4361276197914901644} - m_Layer: 0 + m_Layer: 6 m_Name: Character_FortGolem_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -2350,7 +2350,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 8296497460523017494} - m_Layer: 0 + m_Layer: 6 m_Name: IndexFinger_04 m_TagString: Untagged m_Icon: {fileID: 0} @@ -2382,7 +2382,7 @@ GameObject: m_Component: - component: {fileID: 3590960725534842321} - component: {fileID: 4522993535860619622} - m_Layer: 0 + m_Layer: 6 m_Name: Character_MutantGuy_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -2520,7 +2520,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 2624557334772920005} - m_Layer: 0 + m_Layer: 6 m_Name: Thumb_01 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -2553,7 +2553,7 @@ GameObject: m_Component: - component: {fileID: 294350506880030600} - component: {fileID: 9049566930458412434} - m_Layer: 0 + m_Layer: 6 m_Name: Character_RedDemon_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -2693,7 +2693,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 4543585677225495356} - m_Layer: 0 + m_Layer: 6 m_Name: Finger_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -2725,7 +2725,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 769461243750532741} - m_Layer: 0 + m_Layer: 6 m_Name: Thumb_03 m_TagString: Untagged m_Icon: {fileID: 0} @@ -2756,7 +2756,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 522825655025885808} - m_Layer: 0 + m_Layer: 6 m_Name: IndexFinger_02 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -2788,7 +2788,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 8417583513949358043} - m_Layer: 0 + m_Layer: 6 m_Name: Hand_L m_TagString: Untagged m_Icon: {fileID: 0} @@ -2822,7 +2822,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 2510886572597627575} - m_Layer: 0 + m_Layer: 6 m_Name: UpperLeg_R m_TagString: Untagged m_Icon: {fileID: 0} @@ -2854,7 +2854,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 8586350568747534692} - m_Layer: 0 + m_Layer: 6 m_Name: Hips m_TagString: Untagged m_Icon: {fileID: 0} @@ -2888,7 +2888,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 7751381845014379211} - m_Layer: 0 + m_Layer: 6 m_Name: Shoulder_R m_TagString: Untagged m_Icon: {fileID: 0} @@ -2920,7 +2920,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 5393520628060724146} - m_Layer: 0 + m_Layer: 6 m_Name: Ball_R m_TagString: Untagged m_Icon: {fileID: 0} @@ -2953,7 +2953,7 @@ GameObject: m_Component: - component: {fileID: 6414035440940731888} - component: {fileID: 9069861054110322079} - m_Layer: 0 + m_Layer: 6 m_Name: Character_MechanicalGolem_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -3093,7 +3093,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 393037008073616590} - m_Layer: 0 + m_Layer: 6 m_Name: Spine_02 m_TagString: Untagged m_Icon: {fileID: 0} @@ -3125,7 +3125,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 7070020292155802438} - m_Layer: 0 + m_Layer: 6 m_Name: Finger_02 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -3157,7 +3157,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 728528768444425882} - m_Layer: 0 + m_Layer: 6 m_Name: Finger_02 m_TagString: Untagged m_Icon: {fileID: 0} @@ -3189,7 +3189,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 5783442803983604061} - m_Layer: 0 + m_Layer: 6 m_Name: Thumb_02 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -3221,7 +3221,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 7392812169768159849} - m_Layer: 0 + m_Layer: 6 m_Name: LowerLeg_R m_TagString: Untagged m_Icon: {fileID: 0} @@ -3253,7 +3253,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 540896332569595868} - m_Layer: 0 + m_Layer: 6 m_Name: Finger_03 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -3285,7 +3285,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 9042031077820181245} - m_Layer: 0 + m_Layer: 6 m_Name: Shoulder_L m_TagString: Untagged m_Icon: {fileID: 0} @@ -3317,7 +3317,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6329431241889728063} - m_Layer: 0 + m_Layer: 6 m_Name: Clavicle_R m_TagString: Untagged m_Icon: {fileID: 0} @@ -3349,7 +3349,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6652879279026575357} - m_Layer: 0 + m_Layer: 6 m_Name: Thumb_01 m_TagString: Untagged m_Icon: {fileID: 0} @@ -3381,7 +3381,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 7812724648278576591} - m_Layer: 0 + m_Layer: 6 m_Name: Finger_01 1 m_TagString: Untagged m_Icon: {fileID: 0} @@ -3460,6 +3460,10 @@ PrefabInstance: propertyPath: m_Name value: SM_Prop_Troll_Helmet objectReference: {fileID: 0} + - target: {fileID: 8587111787659894491, guid: 95219051c0a1142448e9ed4beee02fe1, type: 3} + propertyPath: m_Layer + value: 6 + objectReference: {fileID: 0} - target: {fileID: 8587111787659894491, guid: 95219051c0a1142448e9ed4beee02fe1, type: 3} propertyPath: m_IsActive value: 0 diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/ChaseTargetAction.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/ChaseTargetAction.cs index eb5f4408..b02bce42 100644 --- a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/ChaseTargetAction.cs +++ b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/ChaseTargetAction.cs @@ -60,7 +60,6 @@ public partial class ChaseTargetAction : Action return Status.Success; } - // 타겟 위치로 이동 agent.SetDestination(Target.Value.transform.position); return Status.Running; } diff --git a/Assets/_Game/Scripts/Enemy/BossEnemy.cs b/Assets/_Game/Scripts/Enemy/BossEnemy.cs index e4f4003a..1f987dd2 100644 --- a/Assets/_Game/Scripts/Enemy/BossEnemy.cs +++ b/Assets/_Game/Scripts/Enemy/BossEnemy.cs @@ -105,14 +105,11 @@ namespace Colosseum.Enemy customConditions.Clear(); } - private void Update() + protected override void OnServerUpdate() { - if (!IsServer || IsDead || isTransitioning) - return; + if (isTransitioning) return; phaseElapsedTime = Time.time - phaseStartTime; - - // 다음 페이즈 전환 조건 확인 CheckPhaseTransition(); } diff --git a/Assets/_Game/Scripts/Enemy/EnemyBase.cs b/Assets/_Game/Scripts/Enemy/EnemyBase.cs index ae60dec4..faf2a5f0 100644 --- a/Assets/_Game/Scripts/Enemy/EnemyBase.cs +++ b/Assets/_Game/Scripts/Enemy/EnemyBase.cs @@ -30,6 +30,9 @@ namespace Colosseum.Enemy protected NetworkVariable currentMana = new NetworkVariable(50f); protected NetworkVariable isDead = new NetworkVariable(false); + // 플레이어 분리용 (레이어 의존 없이 CharacterController로 식별) + private readonly Collider[] overlapBuffer = new Collider[8]; + // 이벤트 public event Action OnHealthChanged; // currentHealth, maxHealth public event Action OnDamageTaken; // damage @@ -65,6 +68,88 @@ namespace Colosseum.Enemy currentHealth.OnValueChanged += OnHealthChangedInternal; } + protected virtual void Update() + { + if (!IsServer || IsDead) return; + + OnServerUpdate(); + } + + /// + /// 서버 Update 확장 포인트 (하위 클래스에서 override) + /// + protected virtual void OnServerUpdate() { } + + /// + /// NavMeshAgent position sync 및 OnAnimatorMove 이후에 실행됩니다. + /// 보스가 이미 플레이어 안으로 들어온 경우 stoppingDistance 바깥으로 밀어냅니다. + /// Update()에서의 isStopped 조작은 NavMeshAgent에 의해 덮어써지지만, + /// LateUpdate()는 그 이후이므로 확실하게 보정됩니다. + /// + private void LateUpdate() + { + if (!IsServer || IsDead || navMeshAgent == null) return; + + // stoppingDistance가 0이면 radius 기반 fallback 사용 + float stopDist = navMeshAgent.stoppingDistance > 0f + ? navMeshAgent.stoppingDistance + : navMeshAgent.radius + 0.5f; + + int count = Physics.OverlapSphereNonAlloc(transform.position, stopDist, overlapBuffer); + for (int i = 0; i < count; i++) + { + // 레이어 무관하게 CharacterController 유무로 플레이어 식별 + if (!overlapBuffer[i].TryGetComponent(out _)) continue; + + Vector3 toPlayer = overlapBuffer[i].transform.position - transform.position; + toPlayer.y = 0f; + float dist = toPlayer.magnitude; + if (dist >= stopDist) continue; + + // 보스가 실제로 이동 중일 때만 밀어냄. + // isStopped는 수동 설정 시만 true가 되므로, velocity로 실제 이동 여부를 판단. + if (navMeshAgent.velocity.sqrMagnitude > 0.01f) + { + Vector3 pushDir = dist > 0.001f ? -toPlayer.normalized : -transform.forward; + navMeshAgent.Warp(transform.position + pushDir * (stopDist - dist)); + navMeshAgent.isStopped = true; + } + } + } + + /// + /// 보스 스킬 루트모션이 플레이어 방향으로 진입하는 것을 차단합니다. + /// + private void OnAnimatorMove() + { + if (!IsServer || animator == null || navMeshAgent == null) return; + + Vector3 deltaPosition = animator.deltaPosition; + + float blockRadius = Mathf.Max(navMeshAgent.stoppingDistance, navMeshAgent.radius + 0.5f); + int count = Physics.OverlapSphereNonAlloc(transform.position, blockRadius, overlapBuffer); + for (int i = 0; i < count; i++) + { + if (!overlapBuffer[i].TryGetComponent(out _)) continue; + + Vector3 toPlayer = overlapBuffer[i].transform.position - transform.position; + toPlayer.y = 0f; + if (toPlayer.sqrMagnitude < 0.0001f) continue; + + Vector3 deltaXZ = new Vector3(deltaPosition.x, 0f, deltaPosition.z); + if (Vector3.Dot(deltaXZ, toPlayer.normalized) > 0f) + { + deltaPosition.x = 0f; + deltaPosition.z = 0f; + } + } + + navMeshAgent.Move(deltaPosition); + + if (animator.deltaRotation != Quaternion.identity) + transform.rotation *= animator.deltaRotation; + } + public override void OnNetworkDespawn() { currentHealth.OnValueChanged -= OnHealthChangedInternal; diff --git a/Assets/_Game/Scripts/Player/PlayerMovement.cs b/Assets/_Game/Scripts/Player/PlayerMovement.cs index 26038283..43c4662c 100644 --- a/Assets/_Game/Scripts/Player/PlayerMovement.cs +++ b/Assets/_Game/Scripts/Player/PlayerMovement.cs @@ -31,30 +31,16 @@ namespace Colosseum.Player private bool isJumping; private bool wasGrounded; - // 적 충돌 방향 (이동 차단용) + // 적 충돌 차단용 private Vector3 blockedDirection; - private int enemyLayerMask; + private readonly Collider[] overlapBuffer = new Collider[8]; - /// - /// 현재 이동 속도 (애니메이션용) - /// public float CurrentMoveSpeed => moveInput.magnitude * moveSpeed; - - - /// - /// 현재 지면 접촉 상태 - /// public bool IsGrounded => characterController != null ? characterController.isGrounded : false; - - - /// - /// 점프 중 상태 - /// public bool IsJumping => isJumping; public override void OnNetworkSpawn() { - // 로컬 플레이어가 아니면 입력 비활성화 if (!IsOwner) { enabled = false; @@ -62,51 +48,33 @@ namespace Colosseum.Player } characterController = GetComponent(); + // 보스 콜라이더가 겹칠 때 Unity 내부 자동 밀어냄 비활성화. + // 적과의 분리는 EnemyBase.ResolvePlayerOverlap에서 보스 측이 담당. + characterController.enableOverlapRecovery = false; - // SkillController 참조 if (skillController == null) - { skillController = GetComponent(); - } - // Animator 참조 if (animator == null) - { animator = GetComponentInChildren(); - } - // 스폰 포인트에서 위치 설정 SetSpawnPosition(); - - // Input Actions 초기화 InitializeInputActions(); - - // 카메라 설정 SetupCamera(); - // 적 레이어 마스크 초기화 - enemyLayerMask = LayerMask.GetMask("Enemy"); + } - /// - /// 입력 액션 초기화 - /// private void InitializeInputActions() { inputActions = new InputSystem_Actions(); inputActions.Player.Enable(); - // Move 액션 콜백 등록 inputActions.Player.Move.performed += OnMovePerformed; inputActions.Player.Move.canceled += OnMoveCanceled; - - // Jump 액션 콜백 등록 inputActions.Player.Jump.performed += OnJumpPerformed; } - /// - /// 입력 액션 해제 - /// private void CleanupInputActions() { if (inputActions != null) @@ -120,16 +88,12 @@ namespace Colosseum.Player private void OnDisable() { - // 컴포넌트 비활성화 시 입력 해제 CleanupInputActions(); - - // 입력 초기화 moveInput = Vector2.zero; } private void OnEnable() { - // 컴포넌트 재활성화 시 입력 다시 등록 if (IsOwner && inputActions != null) { inputActions.Player.Enable(); @@ -139,16 +103,11 @@ namespace Colosseum.Player } } - /// - /// 스폰 위치 설정 - /// private void SetSpawnPosition() { Transform spawnPoint = PlayerSpawnPoint.GetRandomSpawnPoint(); - if (spawnPoint != null) { - // CharacterController 비활성화 후 위치 설정 (충돌 문제 방지) characterController.enabled = false; transform.position = spawnPoint.position; transform.rotation = spawnPoint.rotation; @@ -156,68 +115,78 @@ namespace Colosseum.Player } } - /// - /// 네트워크 정리 - /// public override void OnNetworkDespawn() { CleanupInputActions(); } - private void OnMovePerformed(InputAction.CallbackContext context) - { - moveInput = context.ReadValue(); - } - - private void OnMoveCanceled(InputAction.CallbackContext context) - { - moveInput = Vector2.zero; - } + private void OnMovePerformed(InputAction.CallbackContext context) => moveInput = context.ReadValue(); + private void OnMoveCanceled(InputAction.CallbackContext context) => moveInput = Vector2.zero; private void OnJumpPerformed(InputAction.CallbackContext context) { if (!isJumping && characterController.isGrounded) - { Jump(); - } } private void SetupCamera() { var cameraController = GetComponent(); if (cameraController == null) - { cameraController = gameObject.AddComponent(); - } cameraController.Initialize(transform, inputActions); } - /// - /// 카메라 재설정 (씬 로드 후 호출) - /// - public void RefreshCamera() - { - SetupCamera(); - } + public void RefreshCamera() => SetupCamera(); private void Update() { if (!IsOwner) return; ApplyGravity(); + UpdateBlockedDirection(); Move(); } + /// + /// 매 프레임 주변 적을 능동적으로 감지하여 blockedDirection을 설정합니다. + /// 콜백 기반이 아니므로 보스가 플레이어 쪽으로 밀고 올 때도 즉시 감지합니다. + /// + private void UpdateBlockedDirection() + { + blockedDirection = Vector3.zero; + + Vector3 center = transform.position + characterController.center; + float radius = characterController.radius + 0.15f; + float halfHeight = Mathf.Max(0f, characterController.height * 0.5f - characterController.radius); + + // 레이어 무관하게 NavMeshAgent 유무로 적 식별 + int count = Physics.OverlapCapsuleNonAlloc( + center + Vector3.up * halfHeight, + center - Vector3.up * halfHeight, + radius, overlapBuffer); + + for (int i = 0; i < count; i++) + { + if (overlapBuffer[i].gameObject == gameObject) continue; + if (!overlapBuffer[i].TryGetComponent(out _)) continue; + + Vector3 toEnemy = overlapBuffer[i].transform.position - transform.position; + toEnemy.y = 0f; + if (toEnemy.sqrMagnitude > 0.0001f) + { + blockedDirection = toEnemy.normalized; + break; + } + } + } + private void ApplyGravity() { if (wasGrounded && velocity.y < 0) - { velocity.y = -2f; - } else - { velocity.y += gravity * Time.deltaTime; - } } private void Move() @@ -227,51 +196,30 @@ namespace Colosseum.Player // 스킬 애니메이션 재생 중에는 이동 불가 (루트 모션은 OnAnimatorMove에서 처리) if (skillController != null && skillController.IsPlayingAnimation) { - // 루트 모션을 사용하지 않는 경우 중력만 적용 if (!skillController.UsesRootMotion) - { characterController.Move(velocity * Time.deltaTime); - } return; } - // 이동 방향 계산 (카메라 기준) Vector3 moveDirection = new Vector3(moveInput.x, 0f, moveInput.y); moveDirection = TransformDirectionByCamera(moveDirection); moveDirection.Normalize(); - // 충돌 방향으로의 이동 차단 (미끄러짐 방지) - if (blockedDirection != Vector3.zero) - { - float blockedAmount = Vector3.Dot(moveDirection, blockedDirection); - if (blockedAmount > 0f) - { - moveDirection -= blockedDirection * blockedAmount; - moveDirection.Normalize(); - } - } + // 적 방향으로 이동 시도 중이면 수평 이동 전체 취소 + if (blockedDirection != Vector3.zero && Vector3.Dot(moveDirection, blockedDirection) > 0f) + moveDirection = Vector3.zero; - // 이동 적용 - Vector3 moveVector = moveDirection * moveSpeed * Time.deltaTime; - characterController.Move(moveVector + velocity * Time.deltaTime); + characterController.Move((moveDirection * moveSpeed + velocity) * Time.deltaTime); - // 충돌 방향 리셋 - blockedDirection = Vector3.zero; - - // 회전 (이동 중일 때만) if (moveDirection != Vector3.zero) { Quaternion targetRotation = Quaternion.LookRotation(moveDirection); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); } - // 착지 체크 (Move 후에 isGrounded가 업데이트됨) if (!wasGrounded && characterController.isGrounded && isJumping) - { OnJumpEnd(); - } - // 다음 프레임을 위해 현재 상태 저장 wasGrounded = characterController.isGrounded; } @@ -280,41 +228,27 @@ namespace Colosseum.Player isJumping = true; velocity.y = jumpForce; - // 애니메이션 컨트롤러에 점프 알림 var animController = GetComponent(); if (animController != null) - { animController.PlayJump(); - } } - /// - /// 점프 중 상태가 끝나면 IsJumping = false; - /// - public void OnJumpEnd() - { - isJumping = false; - } + public void OnJumpEnd() => isJumping = false; private Vector3 TransformDirectionByCamera(Vector3 direction) { if (Camera.main == null) return direction; - Transform cameraTransform = Camera.main.transform; - Vector3 cameraForward = cameraTransform.forward; - Vector3 cameraRight = cameraTransform.right; + Transform cam = Camera.main.transform; + Vector3 forward = new Vector3(cam.forward.x, 0f, cam.forward.z).normalized; + Vector3 right = new Vector3(cam.right.x, 0f, cam.right.z).normalized; - // Y축 제거 - cameraForward.y = 0f; - cameraRight.y = 0f; - cameraForward.Normalize(); - cameraRight.Normalize(); - - return cameraRight * direction.x + cameraForward * direction.z; + return right * direction.x + forward * direction.z; } /// - /// 루트 모션 처리. 스킬 애니메이션 중에 애니메이션의 이동/회전 데이터를 적용합니다. + /// 루트 모션 처리. 스킬 애니메이션 중 애니메이션의 이동/회전 데이터를 적용합니다. + /// 적 방향으로의 이동은 취소합니다. /// private void OnAnimatorMove() { @@ -323,10 +257,19 @@ namespace Colosseum.Player if (skillController == null || !skillController.IsPlayingAnimation) return; if (!skillController.UsesRootMotion) return; - // 루트 모션 이동 적용 Vector3 deltaPosition = animator.deltaPosition; - // Y축 무시 설정 시 중력 유지 + // 적 방향으로 루트 모션이 향하면 수평 이동 취소 + if (blockedDirection != Vector3.zero) + { + Vector3 deltaXZ = new Vector3(deltaPosition.x, 0f, deltaPosition.z); + if (Vector3.Dot(deltaXZ, blockedDirection) > 0f) + { + deltaPosition.x = 0f; + deltaPosition.z = 0f; + } + } + if (skillController.IgnoreRootMotionY) { deltaPosition.y = 0f; @@ -337,57 +280,13 @@ namespace Colosseum.Player characterController.Move(deltaPosition); } - // 루트 모션 회전 적용 if (animator.deltaRotation != Quaternion.identity) - { transform.rotation *= animator.deltaRotation; - } - // 착지 체크 if (!wasGrounded && characterController.isGrounded && isJumping) - { OnJumpEnd(); - } wasGrounded = characterController.isGrounded; } - - /// - /// CharacterController 충돌 처리. 적과 충돌 시 해당 방향 이동을 차단합니다. - /// 충돌 normal을 8방향으로 양자화하여 각진 충돌 느낌을 줍니다. - /// - private void OnControllerColliderHit(ControllerColliderHit hit) - { - // 적과의 충돌인지 확인 - if ((enemyLayerMask & (1 << hit.gameObject.layer)) != 0) - { - // 충돌 방향 저장 (이동 차단용) - blockedDirection = hit.normal; - blockedDirection.y = 0f; - blockedDirection.Normalize(); - - // 8방향으로 양자화 (45도 간격) - blockedDirection = QuantizeToOctagon(blockedDirection); - } - } - - /// - /// 방향을 8각형(45도 간격) 방향으로 양자화합니다. - /// - private Vector3 QuantizeToOctagon(Vector3 direction) - { - if (direction == Vector3.zero) - return direction; - - // 각도 계산 - float angle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg; - - // 45도 단위로 반올림 - float snappedAngle = Mathf.Round(angle / 45f) * 45f; - - // 다시 벡터로 변환 - float radians = snappedAngle * Mathf.Deg2Rad; - return new Vector3(Mathf.Sin(radians), 0f, Mathf.Cos(radians)); - } } }