diff --git a/.plastic/plastic.changes b/.plastic/plastic.changes new file mode 100644 index 00000000..e6e6549b Binary files /dev/null and b/.plastic/plastic.changes differ diff --git a/.plastic/plastic.wktree b/.plastic/plastic.wktree index 745d0655..a07c34ed 100644 Binary files a/.plastic/plastic.wktree and b/.plastic/plastic.wktree differ diff --git a/Assets/0.SCENE/MainGame.unity b/Assets/0.SCENE/MainGame.unity index 51af22f6..4c03038b 100644 --- a/Assets/0.SCENE/MainGame.unity +++ b/Assets/0.SCENE/MainGame.unity @@ -4383,6 +4383,74 @@ Transform: m_CorrespondingSourceObject: {fileID: 4708095961488610, guid: 01d14f7c3a49b4f4e86caee63ef815b1, type: 3} m_PrefabInstance: {fileID: 47955374} m_PrefabAsset: {fileID: 0} +--- !u!1001 &48215575 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 186042788264501781, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_WarningMessage + value: "\nBinding warning: Some generic clip(s) animate transforms that are + already bound by a Humanoid avatar. These transforms can only be changed + by Humanoid clips.\n\tTransform 'chin'\n\tTransform 'pelvis'\n\tTransform + 'index_03_l'\n\tTransform 'lowerarm_l'\n\tTransform 'chin'\n\tTransform 'neck_01'\n\tTransform + 'clavicle_l'\n\tTransform 'ring_02_l'\n\tTransform 'ball_l'\n\tTransform + 'hand_l'\n\tand more ...\n\tFrom animation clip 'idle'\n\tFrom animation + clip 'run'\n\tFrom animation clip 'ghoul_walk'\n\tFrom animation clip 'ghoul_die'\n\tFrom + animation clip 'ghoul_gethit'" + objectReference: {fileID: 0} + - target: {fileID: 5674935864780053661, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_Name + value: SwordMonster + objectReference: {fileID: 0} + - target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_LocalPosition.x + value: 19.30664 + objectReference: {fileID: 0} + - target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_LocalPosition.y + value: 7.250199 + objectReference: {fileID: 0} + - target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_LocalPosition.z + value: 14.687386 + objectReference: {fileID: 0} + - target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3} --- !u!1001 &48315642 PrefabInstance: m_ObjectHideFlags: 0 @@ -9052,14 +9120,14 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 85056388} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3} m_Name: m_EditorClassIdentifier: spawnRange: 15 respawnCooldown: 3 - mobTag: Boss + mobTag: Throw Monster optimizationRange: 60 --- !u!4 &85056390 Transform: @@ -39085,9 +39153,21 @@ MonoBehaviour: impactSpawnPoint: {fileID: 0} counterSystem: {fileID: 358757530} patternInterval: 3 - ironBall: {fileID: 0} - handHolder: {fileID: 0} + attackRange: 3 + ironBall: {fileID: 1550577165} + handHolder: {fileID: 1325714516} bossHealthBar: {fileID: 0} + anim_Roar: Roar + anim_Idle: Monster_Idle + anim_Walk: Monster_Walk + anim_Pickup: Skill_Pickup + anim_Throw: Attack_Throw + anim_DashReady: Skill_Dash_Ready + anim_DashGo: Skill_Dash_Go + anim_SmashCharge: Skill_Smash_Charge + anim_SmashImpact: Skill_Smash_Impact + anim_Sweep: Skill_Sweep + debugState: "\uB300\uAE30 \uC911" --- !u!136 &358757532 CapsuleCollider: m_ObjectHideFlags: 0 @@ -39158,7 +39238,7 @@ Rigidbody: m_UseGravity: 1 m_IsKinematic: 1 m_Interpolate: 0 - m_Constraints: 0 + m_Constraints: 80 m_CollisionDetection: 0 --- !u!1001 &358842824 PrefabInstance: @@ -66018,7 +66098,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!114 &600231663 MonoBehaviour: m_ObjectHideFlags: 0 @@ -66451,8 +66531,8 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3} m_Name: m_EditorClassIdentifier: - healthSource: {fileID: 2112919314} - hpFillImage: {fileID: 910004995} + healthSource: {fileID: 0} + hpFillImage: {fileID: 0} hpText: {fileID: 0} --- !u!1001 &603601734 PrefabInstance: @@ -68541,6 +68621,11 @@ Transform: m_CorrespondingSourceObject: {fileID: 4012526298001850, guid: 1b1f948f132c1cc429df46b11756f4a2, type: 3} m_PrefabInstance: {fileID: 619812415} m_PrefabAsset: {fileID: 0} +--- !u!4 &620355262 stripped +Transform: + m_CorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} + m_PrefabInstance: {fileID: 1101415473} + m_PrefabAsset: {fileID: 0} --- !u!1 &620650552 GameObject: m_ObjectHideFlags: 0 @@ -115622,6 +115707,74 @@ Transform: m_CorrespondingSourceObject: {fileID: 4872071498859388, guid: 5a0d3c35fdb1a0549ab15f2d38859ed1, type: 3} m_PrefabInstance: {fileID: 1080970801} m_PrefabAsset: {fileID: 0} +--- !u!1 &1081393076 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1081393079} + - component: {fileID: 1081393078} + - component: {fileID: 1081393077} + m_Layer: 0 + m_Name: BossTriggerZone + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1081393077 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1081393076} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 086f3fc0675122448a8950f910dc4864, type: 3} + m_Name: + m_EditorClassIdentifier: + boss: {fileID: 358757531} + fogWall: {fileID: 0} +--- !u!65 &1081393078 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1081393076} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 1 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!4 &1081393079 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1081393076} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 26.738499, y: 13.2, z: 32.890816} + m_LocalScale: {x: 17.087, y: 17.087, z: 17.087} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1001 &1083084820 PrefabInstance: m_ObjectHideFlags: 0 @@ -118888,17 +119041,29 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 0} m_Modifications: + - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} + propertyPath: m_LocalScale.x + value: 1.8233726 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} + propertyPath: m_LocalScale.y + value: 1.8233726 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} + propertyPath: m_LocalScale.z + value: 1.8233726 + objectReference: {fileID: 0} - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} propertyPath: m_LocalPosition.x - value: 28.143673 + value: 27.059 objectReference: {fileID: 0} - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} propertyPath: m_LocalPosition.y - value: 5.500213 + value: 5.48 objectReference: {fileID: 0} - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} propertyPath: m_LocalPosition.z - value: 32.754795 + value: 33.461 objectReference: {fileID: 0} - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} propertyPath: m_LocalRotation.w @@ -118932,13 +119097,24 @@ PrefabInstance: propertyPath: m_Name value: BossMonster 3D model objectReference: {fileID: 0} + - target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} - target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} propertyPath: m_Controller value: objectReference: {fileID: 9100000, guid: b207531287111e74d890e730f7278dfa, type: 2} + - target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} + propertyPath: m_ApplyRootMotion + value: 0 + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] - m_AddedGameObjects: [] + m_AddedGameObjects: + - targetCorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} + insertIndex: -1 + addedObject: {fileID: 1325714516} m_AddedComponents: - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} insertIndex: -1 @@ -142858,13 +143034,13 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1325714515} serializedVersion: 2 - m_LocalRotation: {x: -0.0026552675, y: 0.03898671, z: 0.72380346, w: -0.6888988} - m_LocalPosition: {x: 30.757803, y: 8.46146, z: 31.390688} - m_LocalScale: {x: 234.34998, y: 234.35007, z: 234.35002} + m_LocalRotation: {x: -0.038344428, y: 0.70765644, z: -0.0024401005, w: -0.7055112} + m_LocalPosition: {x: 1.304071, y: 1.8877394, z: -1.5619158} + m_LocalScale: {x: 234.34999, y: 234.35004, z: 234.35002} m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1550577166} - m_Father: {fileID: 0} + m_Father: {fileID: 620355262} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1001 &1326407535 PrefabInstance: @@ -165537,8 +165713,8 @@ Transform: m_GameObject: {fileID: 1550577165} serializedVersion: 2 m_LocalRotation: {x: 0.002655249, y: -0.03898664, z: -0.72380346, w: -0.68889886} - m_LocalPosition: {x: 0.00351, y: 0.0007, z: 0.00066} - m_LocalScale: {x: 0.0090075, y: 0.009007501, z: 0.0090075} + m_LocalPosition: {x: 0.00886, y: -0.00478, z: 0.0054} + m_LocalScale: {x: 0.0055073895, y: 0.0055073905, z: 0.0055073895} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1325714516} @@ -228647,6 +228823,22 @@ PrefabInstance: propertyPath: minLength value: 0.2 objectReference: {fileID: 0} + - target: {fileID: 8160405634931995834, guid: c34da37720b95c84887eda34e2d90e5b, type: 3} + propertyPath: playerStats + value: + objectReference: {fileID: 1432447530} + - target: {fileID: 8160405634931995834, guid: c34da37720b95c84887eda34e2d90e5b, type: 3} + propertyPath: OnDieEvent.m_PersistentCalls.m_Calls.Array.size + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8160405634931995834, guid: c34da37720b95c84887eda34e2d90e5b, type: 3} + propertyPath: OnDieEvent.m_PersistentCalls.m_Calls.Array.data[0].m_Target + value: + objectReference: {fileID: 3957690915496532953} + - target: {fileID: 8160405634931995834, guid: c34da37720b95c84887eda34e2d90e5b, type: 3} + propertyPath: OnDieEvent.m_PersistentCalls.m_Calls.Array.data[0].m_CallState + value: 2 + objectReference: {fileID: 0} - target: {fileID: 8345435216483992478, guid: c34da37720b95c84887eda34e2d90e5b, type: 3} propertyPath: m_SlopeLimit value: 47.82 @@ -228659,6 +228851,10 @@ PrefabInstance: propertyPath: normalRange value: 15 objectReference: {fileID: 0} + - target: {fileID: 8965661853020836870, guid: c34da37720b95c84887eda34e2d90e5b, type: 3} + propertyPath: playerStats + value: + objectReference: {fileID: 1432447530} - target: {fileID: 8965661853020836870, guid: c34da37720b95c84887eda34e2d90e5b, type: 3} propertyPath: weaponHitBox value: @@ -229299,9 +229495,10 @@ SceneRoots: - {fileID: 2067270637} - {fileID: 615584948} - {fileID: 200768984} - - {fileID: 1325714516} - {fileID: 1759137305} - {fileID: 198901434} - {fileID: 929255795} - {fileID: 1844125250} - {fileID: 1101415473} + - {fileID: 1081393079} + - {fileID: 48215575} diff --git a/Assets/1.myPrefab/MyMonster/BossMonsterAnimater.controller b/Assets/1.myPrefab/MyMonster/BossMonsterAnimater.controller index cc1013c2..a9503ce2 100644 --- a/Assets/1.myPrefab/MyMonster/BossMonsterAnimater.controller +++ b/Assets/1.myPrefab/MyMonster/BossMonsterAnimater.controller @@ -8,7 +8,13 @@ AnimatorController: m_PrefabAsset: {fileID: 0} m_Name: BossMonsterAnimater serializedVersion: 5 - m_AnimatorParameters: [] + m_AnimatorParameters: + - m_Name: Speed + m_Type: 1 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} m_AnimatorLayers: - serializedVersion: 5 m_Name: Base Layer diff --git a/Assets/1.myPrefab/Player.prefab b/Assets/1.myPrefab/Player.prefab index aa21d6b8..c231e3f2 100644 --- a/Assets/1.myPrefab/Player.prefab +++ b/Assets/1.myPrefab/Player.prefab @@ -1231,6 +1231,9 @@ PrefabInstance: insertIndex: -1 addedObject: {fileID: 7115393052906583336} m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} + insertIndex: -1 + addedObject: {fileID: 8160405634931995834} - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} insertIndex: -1 addedObject: {fileID: -903076858900737616} @@ -1243,9 +1246,6 @@ PrefabInstance: - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} insertIndex: -1 addedObject: {fileID: 8965661853020836870} - - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} - insertIndex: -1 - addedObject: {fileID: 8160405634931995834} - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} insertIndex: -1 addedObject: {fileID: 7686364893196566162} @@ -1273,6 +1273,28 @@ GameObject: m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} m_PrefabInstance: {fileID: 1112926104530361804} m_PrefabAsset: {fileID: 0} +--- !u!114 &8160405634931995834 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 265854990890279069} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 21175cd26e6d59449b495b92d2dbb4ce, type: 3} + m_Name: + m_EditorClassIdentifier: + playerStats: {fileID: 395629277865203624} + invincibleDuration: 1 + OnDieEvent: + m_PersistentCalls: + m_Calls: [] + OnHealthChanged: + m_PersistentCalls: + m_Calls: [] + isInvincible: 0 + isHit: 0 --- !u!114 &-903076858900737616 MonoBehaviour: m_ObjectHideFlags: 0 @@ -1368,23 +1390,6 @@ MonoBehaviour: m_Bits: 0 onlyMaxCharge: 1 defaultProjectilePrefab: {fileID: 0} ---- !u!114 &8160405634931995834 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 265854990890279069} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 21175cd26e6d59449b495b92d2dbb4ce, type: 3} - m_Name: - m_EditorClassIdentifier: - stats: {fileID: 395629277865203624} - animator: {fileID: 6781151982184439901} - attackScript: {fileID: 0} - bossPattern: {fileID: 0} - isInvincible: 0 --- !u!114 &7686364893196566162 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/3D Assets/BossKK.fbx b/Assets/3D Assets/BossKK.fbx new file mode 100644 index 00000000..e305c59f Binary files /dev/null and b/Assets/3D Assets/BossKK.fbx differ diff --git a/Assets/3D Assets/BossKK.fbx.meta b/Assets/3D Assets/BossKK.fbx.meta new file mode 100644 index 00000000..0bc2decf --- /dev/null +++ b/Assets/3D Assets/BossKK.fbx.meta @@ -0,0 +1,109 @@ +fileFormatVersion: 2 +guid: 930e80507da91c14789fbf72fea30274 +ModelImporter: + serializedVersion: 22200 + internalIDToNameTable: [] + externalObjects: {} + materials: + materialImportMode: 2 + materialName: 0 + materialSearch: 1 + materialLocation: 1 + animations: + legacyGenerateAnimations: 4 + bakeSimulation: 0 + resampleCurves: 1 + optimizeGameObjects: 0 + removeConstantScaleCurves: 0 + motionNodeName: + rigImportErrors: + rigImportWarnings: + animationImportErrors: + animationImportWarnings: + animationRetargetingWarnings: + animationDoRetargetingWarnings: 0 + importAnimatedCustomProperties: 0 + importConstraints: 0 + animationCompression: 3 + animationRotationError: 0.5 + animationPositionError: 0.5 + animationScaleError: 0.5 + animationWrapMode: 0 + extraExposedTransformPaths: [] + extraUserProperties: [] + clipAnimations: [] + isReadable: 0 + meshes: + lODScreenPercentages: [] + globalScale: 1 + meshCompression: 0 + addColliders: 0 + useSRGBMaterialColor: 1 + sortHierarchyByName: 1 + importPhysicalCameras: 1 + importVisibility: 1 + importBlendShapes: 1 + importCameras: 1 + importLights: 1 + nodeNameCollisionStrategy: 1 + fileIdsGeneration: 2 + swapUVChannels: 0 + generateSecondaryUV: 0 + useFileUnits: 1 + keepQuads: 0 + weldVertices: 1 + bakeAxisConversion: 0 + preserveHierarchy: 0 + skinWeightsMode: 0 + maxBonesPerVertex: 4 + minBoneWeight: 0.001 + optimizeBones: 1 + meshOptimizationFlags: -1 + indexFormat: 0 + secondaryUVAngleDistortion: 8 + secondaryUVAreaDistortion: 15.000001 + secondaryUVHardAngle: 88 + secondaryUVMarginMethod: 1 + secondaryUVMinLightmapResolution: 40 + secondaryUVMinObjectScale: 1 + secondaryUVPackMargin: 4 + useFileScale: 1 + strictVertexDataChecks: 0 + tangentSpace: + normalSmoothAngle: 60 + normalImportMode: 0 + tangentImportMode: 3 + normalCalculationMode: 4 + legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0 + blendShapeNormalImportMode: 1 + normalSmoothingSource: 0 + referencedClips: [] + importAnimation: 1 + humanDescription: + serializedVersion: 3 + human: [] + skeleton: [] + armTwist: 0.5 + foreArmTwist: 0.5 + upperLegTwist: 0.5 + legTwist: 0.5 + armStretch: 0.05 + legStretch: 0.05 + feetSpacing: 0 + globalScale: 1 + rootMotionBoneName: + hasTranslationDoF: 0 + hasExtraRoot: 1 + skeletonHasParents: 1 + lastHumanDescriptionAvatarSource: {instanceID: 0} + autoGenerateAvatarMappingIfUnspecified: 1 + animationType: 3 + humanoidOversampling: 1 + avatarSetup: 1 + addHumanoidExtraRootOnlyWhenUsingAvatar: 1 + importBlendShapeDeformPercent: 1 + remapMaterialsIfMaterialImportModeIsNone: 0 + additionalBone: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/5.Enemy Animation/BossAnimation'/BossIdle.fbx.meta b/Assets/5.Enemy Animation/BossAnimation'/BossIdle.fbx.meta index a0c8ea9a..128ceeb3 100644 --- a/Assets/5.Enemy Animation/BossAnimation'/BossIdle.fbx.meta +++ b/Assets/5.Enemy Animation/BossAnimation'/BossIdle.fbx.meta @@ -16,8 +16,7 @@ ModelImporter: optimizeGameObjects: 0 removeConstantScaleCurves: 0 motionNodeName: - rigImportErrors: "Copied Avatar Rig Configuration mis-match. Transform hierarchy - does not match:\n\tTransform 'spine' for human bone 'Hips' not found\n" + rigImportErrors: rigImportWarnings: animationImportErrors: animationImportWarnings: @@ -32,7 +31,36 @@ ModelImporter: animationWrapMode: 0 extraExposedTransformPaths: [] extraUserProperties: [] - clipAnimations: [] + clipAnimations: + - serializedVersion: 16 + name: mixamo.com + takeName: mixamo.com + internalID: -203655887218126122 + firstFrame: 0 + lastFrame: 241 + wrapMode: 0 + orientationOffsetY: 0 + level: 0 + cycleOffset: 0 + loop: 0 + hasAdditiveReferencePose: 0 + loopTime: 1 + loopBlend: 0 + loopBlendOrientation: 0 + loopBlendPositionY: 0 + loopBlendPositionXZ: 0 + keepOriginalOrientation: 0 + keepOriginalPositionY: 1 + keepOriginalPositionXZ: 0 + heightFromFeet: 0 + mirror: 0 + bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000 + curves: [] + events: [] + transformMask: [] + maskType: 3 + maskSource: {instanceID: 0} + additiveReferencePoseFrame: 0 isReadable: 0 meshes: lODScreenPercentages: [] @@ -82,8 +110,402 @@ ModelImporter: importAnimation: 1 humanDescription: serializedVersion: 3 - human: [] - skeleton: [] + human: + - boneName: mixamorig:Hips + humanName: Hips + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftUpLeg + humanName: LeftUpperLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightUpLeg + humanName: RightUpperLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftLeg + humanName: LeftLowerLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightLeg + humanName: RightLowerLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftFoot + humanName: LeftFoot + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightFoot + humanName: RightFoot + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine + humanName: Spine + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine1 + humanName: Chest + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Neck + humanName: Neck + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Head + humanName: Head + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftShoulder + humanName: LeftShoulder + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightShoulder + humanName: RightShoulder + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftArm + humanName: LeftUpperArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightArm + humanName: RightUpperArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftForeArm + humanName: LeftLowerArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightForeArm + humanName: RightLowerArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHand + humanName: LeftHand + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHand + humanName: RightHand + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftToeBase + humanName: LeftToes + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightToeBase + humanName: RightToes + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex1 + humanName: Left Index Proximal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex2 + humanName: Left Index Intermediate + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex3 + humanName: Left Index Distal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex1 + humanName: Right Index Proximal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex2 + humanName: Right Index Intermediate + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex3 + humanName: Right Index Distal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine2 + humanName: UpperChest + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + skeleton: + - name: BossIdle(Clone) + parentName: + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Hips + parentName: BossIdle(Clone) + position: {x: 0.0026833098, y: 0.9943079, z: -0.012676053} + rotation: {x: -0.002174467, y: -0.0005086692, z: 0.0057661077, w: 0.9999809} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine + parentName: mixamorig:Hips + position: {x: -0, y: 0.1171, z: 0.0029} + rotation: {x: 0.1565307, y: -0.00068456325, z: 0.000030100546, w: 0.98767287} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine1 + parentName: mixamorig:Spine + position: {x: -0, y: 0.1367, z: 0} + rotation: {x: 0.09012192, y: -0.0022969642, z: -0.007762773, w: 0.9958978} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine2 + parentName: mixamorig:Spine1 + position: {x: -0, y: 0.1562, z: 0} + rotation: {x: -0.0013413654, y: -0.0019386135, z: -0.0049632005, w: 0.9999849} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Neck + parentName: mixamorig:Spine2 + position: {x: -0, y: 0.1758, z: 0} + rotation: {x: -0.37416202, y: 0.0052613267, z: 0.0064393664, w: 0.927326} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Head + parentName: mixamorig:Neck + position: {x: -0, y: 0.0536, z: 0.0305} + rotation: {x: -0.18870564, y: -0.0236753, z: 0.017435722, w: 0.98159343} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:HeadTop_End + parentName: mixamorig:Head + position: {x: -0, y: 0.1727, z: 0.0982} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftShoulder + parentName: mixamorig:Spine2 + position: {x: -0.064, y: 0.1358, z: -0.0078} + rotation: {x: 0.6866209, y: -0.46074542, z: 0.44875145, w: 0.33895078} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftArm + parentName: mixamorig:LeftShoulder + position: {x: -0, y: 0.1556, z: 0} + rotation: {x: -0.13769072, y: -0.0002792626, z: 0.060832813, w: 0.9886054} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftForeArm + parentName: mixamorig:LeftArm + position: {x: -0, y: 0.2133, z: 0} + rotation: {x: -0.005401833, y: -0.06163869, z: 0.0028284462, w: 0.99807996} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHand + parentName: mixamorig:LeftForeArm + position: {x: -0, y: 0.3139, z: 0} + rotation: {x: -0.06714013, y: 0.43071648, z: -0.18811981, w: 0.8801059} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex1 + parentName: mixamorig:LeftHand + position: {x: -0, y: 0.0657, z: 0} + rotation: {x: 0.05516846, y: -0.65295166, z: -0.05171242, w: 0.75361556} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex2 + parentName: mixamorig:LeftHandIndex1 + position: {x: -0, y: 0.0705, z: 0} + rotation: {x: 0.3534335, y: -0.012413347, z: -0.101619236, w: 0.929841} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex3 + parentName: mixamorig:LeftHandIndex2 + position: {x: -0.0092, y: 0.0512, z: -0.0468} + rotation: {x: -0.13607779, y: 0.1358576, z: 0.17146648, w: 0.9662426} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex4 + parentName: mixamorig:LeftHandIndex3 + position: {x: 0.0269, y: 0.0618, z: -0.0313} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightShoulder + parentName: mixamorig:Spine2 + position: {x: 0.064, y: 0.136, z: 0.0016} + rotation: {x: 0.67744666, y: 0.47600952, z: -0.42826772, w: 0.36203295} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightArm + parentName: mixamorig:RightShoulder + position: {x: -0, y: 0.1546, z: 0} + rotation: {x: -0.13277586, y: -0.21806774, z: -0.04718864, w: 0.9657072} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightForeArm + parentName: mixamorig:RightArm + position: {x: -0, y: 0.2128, z: 0} + rotation: {x: -0.017916152, y: -0.14330766, z: 0.0014052424, w: 0.989515} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHand + parentName: mixamorig:RightForeArm + position: {x: -0, y: 0.3137, z: 0} + rotation: {x: 0.0841442, y: -0.13422275, z: -0.006523322, w: 0.9873507} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex1 + parentName: mixamorig:RightHand + position: {x: -0, y: 0.2098, z: 0} + rotation: {x: 0.06868836, y: 0.15733252, z: -0.014478484, w: 0.98504764} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex2 + parentName: mixamorig:RightHandIndex1 + position: {x: -0, y: 0.0172, z: 0} + rotation: {x: -0.07102833, y: -0.010314832, z: -0.035351228, w: 0.99679434} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex3 + parentName: mixamorig:RightHandIndex2 + position: {x: -0.0018, y: 0.0213, z: 0.0043} + rotation: {x: 0.07075478, y: -0.04816231, z: 0.077679224, w: 0.99329764} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex4 + parentName: mixamorig:RightHandIndex3 + position: {x: 0.0007, y: 0.0188, z: 0.0021} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftUpLeg + parentName: mixamorig:Hips + position: {x: -0.0906, y: -0.0651, z: 0.0026} + rotation: {x: -0.13512881, y: 0.07888633, z: 0.9815783, w: 0.10964156} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftLeg + parentName: mixamorig:LeftUpLeg + position: {x: -0, y: 0.392, z: 0} + rotation: {x: -0.08549402, y: -0.67984086, z: 0.29791063, w: 0.6646476} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftFoot + parentName: mixamorig:LeftLeg + position: {x: -0, y: 0.4038, z: 0} + rotation: {x: 0.37818304, y: 0.57879406, z: -0.44982204, w: 0.5653628} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftToeBase + parentName: mixamorig:LeftFoot + position: {x: -0, y: 0.2346, z: 0} + rotation: {x: 0.3567435, y: -0.07525214, z: 0.12925003, w: 0.92215276} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftToe_End + parentName: mixamorig:LeftToeBase + position: {x: -0, y: 0.1139, z: 0} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightUpLeg + parentName: mixamorig:Hips + position: {x: 0.0906, y: -0.0651, z: 0.0037} + rotation: {x: 0.21856742, y: 0.09107618, z: 0.9678308, w: -0.08507111} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightLeg + parentName: mixamorig:RightUpLeg + position: {x: -0, y: 0.3914, z: 0} + rotation: {x: -0.29833734, y: -0.49995032, z: 0.099081814, w: 0.8069866} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightFoot + parentName: mixamorig:RightLeg + position: {x: -0, y: 0.4064, z: 0} + rotation: {x: 0.4983791, y: 0.46412635, z: -0.31939918, w: 0.6589303} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightToeBase + parentName: mixamorig:RightFoot + position: {x: -0, y: 0.2331, z: 0} + rotation: {x: 0.3513939, y: -0.029390866, z: -0.13634582, w: 0.9257798} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightToe_End + parentName: mixamorig:RightToeBase + position: {x: -0, y: 0.1125, z: 0} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} armTwist: 0.5 foreArmTwist: 0.5 upperLegTwist: 0.5 diff --git a/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller b/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller new file mode 100644 index 00000000..9e9bb287 --- /dev/null +++ b/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller @@ -0,0 +1,95 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1101 &-5377988278109688191 +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: 0} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 1 + serializedVersion: 3 + m_TransitionDuration: 0.25 + m_TransitionOffset: 0 + m_ExitTime: 0.93775934 + m_HasExitTime: 1 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1107 &-3325615192832722807 +AnimatorStateMachine: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Base Layer + m_ChildStates: + - serializedVersion: 1 + m_State: {fileID: 5540596969216719168} + m_Position: {x: 320, y: 20, z: 0} + m_ChildStateMachines: [] + m_AnyStateTransitions: [] + m_EntryTransitions: [] + m_StateMachineTransitions: {} + m_StateMachineBehaviours: [] + m_AnyStatePosition: {x: 50, y: 20, z: 0} + m_EntryPosition: {x: 50, y: 120, z: 0} + m_ExitPosition: {x: 800, y: 120, z: 0} + m_ParentStateMachinePosition: {x: 800, y: 20, z: 0} + m_DefaultState: {fileID: 5540596969216719168} +--- !u!91 &9100000 +AnimatorController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: BossKK + serializedVersion: 5 + m_AnimatorParameters: [] + m_AnimatorLayers: + - serializedVersion: 5 + m_Name: Base Layer + m_StateMachine: {fileID: -3325615192832722807} + m_Mask: {fileID: 0} + m_Motions: [] + m_Behaviours: [] + m_BlendingMode: 0 + m_SyncedLayerIndex: -1 + m_DefaultWeight: 0 + m_IKPass: 0 + m_SyncedLayerAffectsTiming: 0 + m_Controller: {fileID: 9100000} +--- !u!1102 &5540596969216719168 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: mixamo_com + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: + - {fileID: -5377988278109688191} + 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: -203655887218126122, guid: dc29be49c4c60f74899f0410ec955195, type: 3} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: diff --git a/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller.meta b/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller.meta new file mode 100644 index 00000000..a3bc8989 --- /dev/null +++ b/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bff4e8c104f01194fb159c6c309b3a49 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 9100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/5.Enemy Animation/BossAnimation'/BossWalk.fbx.meta b/Assets/5.Enemy Animation/BossAnimation'/BossWalk.fbx.meta index 9e00889a..ed53c3e8 100644 --- a/Assets/5.Enemy Animation/BossAnimation'/BossWalk.fbx.meta +++ b/Assets/5.Enemy Animation/BossAnimation'/BossWalk.fbx.meta @@ -16,8 +16,7 @@ ModelImporter: optimizeGameObjects: 0 removeConstantScaleCurves: 0 motionNodeName: - rigImportErrors: "Copied Avatar Rig Configuration mis-match. Transform hierarchy - does not match:\n\tTransform 'spine' for human bone 'Hips' not found\n" + rigImportErrors: rigImportWarnings: animationImportErrors: animationImportWarnings: @@ -32,7 +31,36 @@ ModelImporter: animationWrapMode: 0 extraExposedTransformPaths: [] extraUserProperties: [] - clipAnimations: [] + clipAnimations: + - serializedVersion: 16 + name: mixamo.com + takeName: mixamo.com + internalID: -203655887218126122 + firstFrame: 0 + lastFrame: 85 + wrapMode: 0 + orientationOffsetY: 0 + level: 0 + cycleOffset: 0 + loop: 0 + hasAdditiveReferencePose: 0 + loopTime: 1 + loopBlend: 0 + loopBlendOrientation: 0 + loopBlendPositionY: 0 + loopBlendPositionXZ: 0 + keepOriginalOrientation: 0 + keepOriginalPositionY: 1 + keepOriginalPositionXZ: 0 + heightFromFeet: 0 + mirror: 0 + bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000 + curves: [] + events: [] + transformMask: [] + maskType: 3 + maskSource: {instanceID: 0} + additiveReferencePoseFrame: 0 isReadable: 0 meshes: lODScreenPercentages: [] @@ -82,8 +110,402 @@ ModelImporter: importAnimation: 1 humanDescription: serializedVersion: 3 - human: [] - skeleton: [] + human: + - boneName: mixamorig:Hips + humanName: Hips + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftUpLeg + humanName: LeftUpperLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightUpLeg + humanName: RightUpperLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftLeg + humanName: LeftLowerLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightLeg + humanName: RightLowerLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftFoot + humanName: LeftFoot + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightFoot + humanName: RightFoot + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine + humanName: Spine + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine1 + humanName: Chest + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Neck + humanName: Neck + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Head + humanName: Head + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftShoulder + humanName: LeftShoulder + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightShoulder + humanName: RightShoulder + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftArm + humanName: LeftUpperArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightArm + humanName: RightUpperArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftForeArm + humanName: LeftLowerArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightForeArm + humanName: RightLowerArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHand + humanName: LeftHand + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHand + humanName: RightHand + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftToeBase + humanName: LeftToes + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightToeBase + humanName: RightToes + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex1 + humanName: Left Index Proximal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex2 + humanName: Left Index Intermediate + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex3 + humanName: Left Index Distal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex1 + humanName: Right Index Proximal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex2 + humanName: Right Index Intermediate + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex3 + humanName: Right Index Distal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine2 + humanName: UpperChest + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + skeleton: + - name: BossWalk(Clone) + parentName: + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Hips + parentName: BossWalk(Clone) + position: {x: 0.0025809966, y: 0.9786751, z: -0.010516135} + rotation: {x: -0.06791151, y: 0.046682756, z: -0.025422283, w: 0.9962743} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine + parentName: mixamorig:Hips + position: {x: -0, y: 0.1171, z: 0.0029} + rotation: {x: 0.052984815, y: -0.010016891, z: 0.016819054, w: 0.99840343} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine1 + parentName: mixamorig:Spine + position: {x: -0, y: 0.1367, z: 0} + rotation: {x: 0.08616688, y: -0.018176258, z: 0.023615148, w: 0.99583495} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine2 + parentName: mixamorig:Spine1 + position: {x: -0, y: 0.1562, z: 0} + rotation: {x: 0.08652799, y: -0.016902944, z: 0.023167178, w: 0.99583656} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Neck + parentName: mixamorig:Spine2 + position: {x: -0, y: 0.1758, z: 0} + rotation: {x: -0.17461915, y: 0.03961615, z: -0.02823548, w: 0.9834335} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Head + parentName: mixamorig:Neck + position: {x: -0, y: 0.0536, z: 0.0305} + rotation: {x: -0.18009952, y: 0.088830575, z: 0.009434662, w: 0.97958374} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:HeadTop_End + parentName: mixamorig:Head + position: {x: -0, y: 0.1727, z: 0.0982} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftShoulder + parentName: mixamorig:Spine2 + position: {x: -0.064, y: 0.1358, z: -0.0078} + rotation: {x: 0.54345345, y: -0.44411075, z: 0.5735583, w: 0.4224394} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftArm + parentName: mixamorig:LeftShoulder + position: {x: -0, y: 0.1556, z: 0} + rotation: {x: -0.14027706, y: -0.238704, z: -0.008420287, w: 0.96087044} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftForeArm + parentName: mixamorig:LeftArm + position: {x: -0, y: 0.2133, z: 0} + rotation: {x: -0.016032558, y: 0.018755268, z: 0.011540319, w: 0.999629} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHand + parentName: mixamorig:LeftForeArm + position: {x: -0, y: 0.3139, z: 0} + rotation: {x: 0.016468085, y: 0.11859691, z: -0.113853276, w: 0.98625606} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex1 + parentName: mixamorig:LeftHand + position: {x: -0, y: 0.0657, z: 0} + rotation: {x: 0.043096147, y: -0.6442412, z: -0.05172926, w: 0.76185304} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex2 + parentName: mixamorig:LeftHandIndex1 + position: {x: -0, y: 0.0705, z: 0} + rotation: {x: 0.35360065, y: -0.012077208, z: -0.100781396, w: 0.92987305} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex3 + parentName: mixamorig:LeftHandIndex2 + position: {x: -0.0092, y: 0.0512, z: -0.0468} + rotation: {x: -0.1376931, y: 0.13756303, z: 0.16533016, w: 0.96684176} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex4 + parentName: mixamorig:LeftHandIndex3 + position: {x: 0.0269, y: 0.0618, z: -0.0313} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightShoulder + parentName: mixamorig:Spine2 + position: {x: 0.064, y: 0.136, z: 0.0016} + rotation: {x: 0.60436624, y: 0.39707544, z: -0.5637547, w: 0.3990654} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightArm + parentName: mixamorig:RightShoulder + position: {x: -0, y: 0.1546, z: 0} + rotation: {x: -0.12594903, y: 0.034397986, z: -0.037340727, w: 0.9907368} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightForeArm + parentName: mixamorig:RightArm + position: {x: -0, y: 0.2128, z: 0} + rotation: {x: -0.027492605, y: -0.060033023, z: 0.02598758, w: 0.9974792} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHand + parentName: mixamorig:RightForeArm + position: {x: -0, y: 0.3137, z: 0} + rotation: {x: 0.084213026, y: -0.1548261, z: -0.020946275, w: 0.9841231} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex1 + parentName: mixamorig:RightHand + position: {x: -0, y: 0.2098, z: 0} + rotation: {x: 0.064756036, y: -0.025416251, z: -0.020036412, w: 0.9973762} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex2 + parentName: mixamorig:RightHandIndex1 + position: {x: -0, y: 0.0172, z: 0} + rotation: {x: -0.066692896, y: 0.001831721, z: -0.046279315, w: 0.9966981} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex3 + parentName: mixamorig:RightHandIndex2 + position: {x: -0.0018, y: 0.0213, z: 0.0043} + rotation: {x: 0.07979187, y: -0.0087349545, z: 0.05717984, w: 0.9951319} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex4 + parentName: mixamorig:RightHandIndex3 + position: {x: 0.0007, y: 0.0188, z: 0.0021} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftUpLeg + parentName: mixamorig:Hips + position: {x: -0.0906, y: -0.0651, z: 0.0026} + rotation: {x: -0.092424944, y: 0.04025192, z: 0.9944845, w: 0.028948884} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftLeg + parentName: mixamorig:LeftUpLeg + position: {x: -0, y: 0.392, z: 0} + rotation: {x: -0.14082664, y: -0.72221404, z: 0.29314968, w: 0.61044085} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftFoot + parentName: mixamorig:LeftLeg + position: {x: -0, y: 0.4038, z: 0} + rotation: {x: 0.3932538, y: 0.6461467, z: -0.377344, w: 0.53428215} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftToeBase + parentName: mixamorig:LeftFoot + position: {x: -0, y: 0.2346, z: 0} + rotation: {x: 0.3386809, y: -0.052146785, z: 0.01728521, w: 0.9392961} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftToe_End + parentName: mixamorig:LeftToeBase + position: {x: -0, y: 0.1139, z: 0} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightUpLeg + parentName: mixamorig:Hips + position: {x: 0.0906, y: -0.0651, z: 0.0037} + rotation: {x: 0.10251077, y: 0.036421414, z: 0.98532605, w: -0.1315201} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightLeg + parentName: mixamorig:RightUpLeg + position: {x: -0, y: 0.3914, z: 0} + rotation: {x: -0.29762018, y: -0.6152928, z: 0.0053073, w: 0.7299377} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightFoot + parentName: mixamorig:RightLeg + position: {x: -0, y: 0.4064, z: 0} + rotation: {x: 0.5381931, y: 0.48825583, z: -0.36212963, w: 0.5837951} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightToeBase + parentName: mixamorig:RightFoot + position: {x: -0, y: 0.2331, z: 0} + rotation: {x: 0.33407575, y: 0.04256158, z: -0.014975957, w: 0.9414657} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightToe_End + parentName: mixamorig:RightToeBase + position: {x: -0, y: 0.1125, z: 0} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} armTwist: 0.5 foreArmTwist: 0.5 upperLegTwist: 0.5 diff --git a/Assets/Scripts/Camera/Effects/CamShake.cs b/Assets/Scripts/Camera/Effects/CamShake.cs index d57d2c9b..fdcb0d9f 100644 --- a/Assets/Scripts/Camera/Effects/CamShake.cs +++ b/Assets/Scripts/Camera/Effects/CamShake.cs @@ -1,78 +1,80 @@ -using UnityEngine; -using Cinemachine; -using System.Collections; +using UnityEngine; // 유니티 기본 기능(Time, Vector3, Coroutine 등)을 쓰기 위해 불러올거에요 -> UnityEngine를 +using Cinemachine; // 시네머신 카메라/임펄스 기능을 쓰기 위해 불러올거에요 -> Cinemachine을 +using System.Collections; // 코루틴(IEnumerator)을 쓰기 위해 불러올거에요 -> System.Collections를 -public class CinemachineShake : MonoBehaviour -{ - public static CinemachineShake Instance { get; private set; } +public class CinemachineShake : MonoBehaviour // 클래스를 선언할거에요 -> 카메라 흔들림/줌/히트슬로우를 담당하는 CinemachineShake를 +{ // 코드 블록을 시작할거에요 -> CinemachineShake 범위를 - [Header("--- 컴포넌트 연결 ---")] - private CinemachineImpulseSource _impulseSource; - [SerializeField] private CinemachineVirtualCamera _vCam; + public static CinemachineShake Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스를 외부에서 읽기만 가능하게 - [Header("--- 줌 설정 ---")] - [SerializeField] private float defaultFOV = 60f; - [SerializeField] private float zoomedFOV = 45f; - [SerializeField] private float zoomSpeed = 5f; + [Header("--- 컴포넌트 연결 ---")] // 인스펙터에 제목을 표시할거에요 -> 컴포넌트 연결 섹션을 + private CinemachineImpulseSource _impulseSource; // 변수를 선언할거에요 -> 임펄스(카메라 흔들림) 발사기 컴포넌트를 담을 _impulseSource를 + [SerializeField] private CinemachineVirtualCamera _vCam; // 변수를 선언할거에요 -> 조절할 가상 카메라를 담을 _vCam을 - private Coroutine _zoomCoroutine; + [Header("--- 줌 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> 줌 설정 섹션을 + [SerializeField] private float defaultFOV = 60f; // 변수를 선언할거에요 -> 기본 FOV를 defaultFOV에 + [SerializeField] private float zoomedFOV = 45f; // 변수를 선언할거에요 -> 줌인 FOV를 zoomedFOV에 + [SerializeField] private float zoomSpeed = 5f; // 변수를 선언할거에요 -> 줌 보간 속도를 zoomSpeed에 - private void Awake() - { - Instance = this; - _impulseSource = GetComponent(); - if (_vCam == null) _vCam = GetComponent(); - } + private Coroutine _zoomCoroutine; // 변수를 선언할거에요 -> 현재 실행 중인 줌 코루틴을 담을 _zoomCoroutine을 - // ⭐ 렉 없는 타격감: 시간을 0.1배속으로 늘림 (Hit-Slow) - public void HitSlow(float duration = 0.15f, float timeScale = 0.1f) - { - StartCoroutine(DoHitSlow(duration, timeScale)); - } + private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + Instance = this; // 값을 넣을거에요 -> 싱글톤 인스턴스를 자기 자신으로 설정 + _impulseSource = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 오브젝트에서 임펄스 소스를 찾아 저장 + if (_vCam == null) _vCam = GetComponent(); // 조건이 맞으면 실행할거에요 -> vCam이 비어있으면 내 오브젝트에서 찾아 채우기 + } // 코드 블록을 끝낼거에요 -> Awake를 - private IEnumerator DoHitSlow(float duration, float timeScale) - { - Time.timeScale = timeScale; // 멈추지 않고 아주 느리게 - yield return new WaitForSecondsRealtime(duration); - Time.timeScale = 1.0f; - } + // ⭐ 렉 없는 타격감: 시간을 0.1배속으로 늘림 (Hit-Slow) // 설명을 적을거에요 -> 짧게 슬로모션을 걸어 타격감을 올리는 기능 + public void HitSlow(float duration = 0.15f, float timeScale = 0.1f) // 함수를 선언할거에요 -> duration/timeScale로 히트슬로우를 주는 HitSlow를 + { // 코드 블록을 시작할거에요 -> HitSlow 범위를 + StartCoroutine(DoHitSlow(duration, timeScale)); // 코루틴을 실행할거에요 -> 실제 슬로우 처리를 하는 DoHitSlow를 + } // 코드 블록을 끝낼거에요 -> HitSlow를 - // ⭐ 카메라 킥: 순간적으로 FOV를 확 줄여 충격을 줌 - public void CameraKick(float kickAmount = 8f) - { - if (_vCam == null) return; - _vCam.m_Lens.FieldOfView -= kickAmount; // 순간 줌인 - SetZoom(false); // AnimateZoom을 통해 부드럽게 복구 - } + private IEnumerator DoHitSlow(float duration, float timeScale) // 코루틴 함수를 선언할거에요 -> 히트슬로우를 수행하는 DoHitSlow를 + { // 코드 블록을 시작할거에요 -> DoHitSlow 범위를 + Time.timeScale = timeScale; // 값을 바꿀거에요 -> 전체 게임 시간을 느리게(멈춤은 아님) + yield return new WaitForSecondsRealtime(duration); // 기다릴거에요 -> 실제 시간 기준으로 duration만큼 + Time.timeScale = 1.0f; // 값을 복구할거에요 -> 게임 시간을 정상(1)으로 + } // 코드 블록을 끝낼거에요 -> DoHitSlow를 - public void ShakeAttack() - { - if (_impulseSource == null) return; - _impulseSource.GenerateImpulse(Vector3.down * 1.5f); - } + // ⭐ 카메라 킥: 순간적으로 FOV를 확 줄여 충격을 줌 // 설명을 적을거에요 -> 순간 줌인 후 부드럽게 복구시키는 연출 + public void CameraKick(float kickAmount = 8f) // 함수를 선언할거에요 -> FOV를 순간 감소시키는 CameraKick을 + { // 코드 블록을 시작할거에요 -> CameraKick 범위를 + if (_vCam == null) return; // 조건이 맞으면 종료할거에요 -> 카메라가 없으면 처리 불가 + _vCam.m_Lens.FieldOfView -= kickAmount; // 값을 바꿀거에요 -> FOV를 줄여서 순간 줌인 효과 + SetZoom(false); // 함수를 실행할거에요 -> 기본 FOV로 부드럽게 돌아가도록 줌 애니메이션 시작 + } // 코드 블록을 끝낼거에요 -> CameraKick을 - public void ShakeNoNo() - { - if (_impulseSource == null) return; - _impulseSource.GenerateImpulse(Vector3.right * 0.4f); - } + public void ShakeAttack() // 함수를 선언할거에요 -> 공격용 흔들림을 주는 ShakeAttack을 + { // 코드 블록을 시작할거에요 -> ShakeAttack 범위를 + if (_impulseSource == null) return; // 조건이 맞으면 종료할거에요 -> 임펄스 소스가 없으면 불가 + _impulseSource.GenerateImpulse(Vector3.down * 1.5f); // 임펄스를 발생시킬거에요 -> 아래 방향으로 강하게 흔들기 + } // 코드 블록을 끝낼거에요 -> ShakeAttack을 - public void SetZoom(bool isZooming) - { - if (_vCam == null) return; - if (_zoomCoroutine != null) StopCoroutine(_zoomCoroutine); - float targetFOV = isZooming ? zoomedFOV : defaultFOV; - _zoomCoroutine = StartCoroutine(AnimateZoom(targetFOV)); - } + public void ShakeNoNo() // 함수를 선언할거에요 -> 좌우로 살짝 흔드는 ShakeNoNo를 + { // 코드 블록을 시작할거에요 -> ShakeNoNo 범위를 + if (_impulseSource == null) return; // 조건이 맞으면 종료할거에요 -> 임펄스 소스가 없으면 불가 + _impulseSource.GenerateImpulse(Vector3.right * 0.4f); // 임펄스를 발생시킬거에요 -> 오른쪽 방향으로 약하게 흔들기 + } // 코드 블록을 끝낼거에요 -> ShakeNoNo를 - private IEnumerator AnimateZoom(float target) - { - if (_vCam == null) yield break; - while (Mathf.Abs(_vCam.m_Lens.FieldOfView - target) > 0.1f) - { - _vCam.m_Lens.FieldOfView = Mathf.Lerp(_vCam.m_Lens.FieldOfView, target, Time.deltaTime * zoomSpeed); - yield return null; - } - _vCam.m_Lens.FieldOfView = target; - } -} \ No newline at end of file + public void SetZoom(bool isZooming) // 함수를 선언할거에요 -> 줌인/줌아웃 목표를 설정하는 SetZoom을 + { // 코드 블록을 시작할거에요 -> SetZoom 범위를 + if (_vCam == null) return; // 조건이 맞으면 종료할거에요 -> 카메라가 없으면 불가 + if (_zoomCoroutine != null) StopCoroutine(_zoomCoroutine); // 조건이 맞으면 실행할거에요 -> 기존 줌 코루틴이 있으면 중지해서 중첩 방지 + float targetFOV = isZooming ? zoomedFOV : defaultFOV; // 목표 FOV를 정할거에요 -> 줌이면 zoomedFOV, 아니면 defaultFOV + _zoomCoroutine = StartCoroutine(AnimateZoom(targetFOV)); // 코루틴을 실행할거에요 -> 목표 FOV로 부드럽게 이동하는 AnimateZoom을 + } // 코드 블록을 끝낼거에요 -> SetZoom을 + + private IEnumerator AnimateZoom(float target) // 코루틴 함수를 선언할거에요 -> FOV를 목표값까지 보간하는 AnimateZoom을 + { // 코드 블록을 시작할거에요 -> AnimateZoom 범위를 + if (_vCam == null) yield break; // 조건이 맞으면 중단할거에요 -> 카메라가 없으면 코루틴 종료 + while (Mathf.Abs(_vCam.m_Lens.FieldOfView - target) > 0.1f) // 반복할거에요 -> 현재 FOV가 목표값과 충분히 가까워질 때까지 + { // 코드 블록을 시작할거에요 -> 보간 루프 + _vCam.m_Lens.FieldOfView = Mathf.Lerp(_vCam.m_Lens.FieldOfView, target, Time.deltaTime * zoomSpeed); // 값을 보간할거에요 -> 현재 FOV를 target 쪽으로 부드럽게 + yield return null; // 다음 프레임까지 기다릴거에요 -> 프레임 단위로 갱신 + } // 코드 블록을 끝낼거에요 -> 보간 루프 + _vCam.m_Lens.FieldOfView = target; // 값을 확정할거에요 -> 마지막에 정확히 target으로 맞추기 + } // 코드 블록을 끝낼거에요 -> AnimateZoom을 + +} // 코드 블록을 끝낼거에요 -> CinemachineShake를 \ No newline at end of file diff --git a/Assets/Scripts/Camera/PlayerAim.cs b/Assets/Scripts/Camera/PlayerAim.cs index 99a94151..00797a47 100644 --- a/Assets/Scripts/Camera/PlayerAim.cs +++ b/Assets/Scripts/Camera/PlayerAim.cs @@ -1,35 +1,35 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class PlayerAim : MonoBehaviour +public class PlayerAim : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerAim을 { - [SerializeField] private PlayerHealth health; + [SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 플레이어 체력 스크립트를 - [Header("회전 설정")] - [SerializeField] private float rotationSpeed = 15f; // 감도 조절 - [SerializeField] private float rotationDeadzone = 1.2f; // 근접전 '뒤돌기' 방지 범위 + [Header("회전 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 회전 설정 을 + [SerializeField] private float rotationSpeed = 15f; // 변수를 선언할거에요 -> 회전 속도(감도)를 + [SerializeField] private float rotationDeadzone = 1.2f; // 변수를 선언할거에요 -> 회전 무시 범위(데드존)를 - public void RotateTowardsMouse() + public void RotateTowardsMouse() // 함수를 선언할거에요 -> 마우스 방향으로 회전하는 RotateTowardsMouse를 { - if (health != null && health.IsDead) return; + if (health != null && health.IsDead) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 죽었다면 // 1. 캐릭터의 월드 위치를 화면 상의 2D 좌표로 변환합니다. - Vector3 playerScreenPos = Camera.main.WorldToScreenPoint(transform.position); + Vector3 playerScreenPos = Camera.main.WorldToScreenPoint(transform.position); // 변환할거에요 -> 캐릭터 월드 좌표를 화면 좌표로 // 2. 마우스의 화면 좌표를 가져옵니다. - Vector3 mousePos = Input.mousePosition; + Vector3 mousePos = Input.mousePosition; // 가져올거에요 -> 현재 마우스 위치를 // 3. 화면상에서 플레이어 -> 마우스로 향하는 2D 방향 벡터를 구합니다. - Vector3 dir = mousePos - playerScreenPos; + Vector3 dir = mousePos - playerScreenPos; // 계산할거에요 -> 마우스와 플레이어 사이의 방향 벡터를 // 4. 거리가 너무 가까우면 회전을 무시합니다 (데드존 적용). - if (dir.magnitude < rotationDeadzone * 50f) return; + if (dir.magnitude < rotationDeadzone * 50f) return; // 조건이 맞으면 중단할거에요 -> 마우스가 캐릭터에 너무 가깝다면 // 5. 2D 방향 벡터를 각도(Atan2)로 변환합니다. // 화면의 X는 월드의 X, 화면의 Y는 월드의 Z에 대응합니다 (쿼터뷰/탑다운 기준). - float angle = Mathf.Atan2(dir.x, dir.y) * Mathf.Rad2Deg; + float angle = Mathf.Atan2(dir.x, dir.y) * Mathf.Rad2Deg; // 계산할거에요 -> 벡터를 각도로 변환해서 // 6. 계산된 각도로 캐릭터를 부드럽게 회전시킵니다. - Quaternion targetRotation = Quaternion.Euler(0, angle, 0); - transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); + Quaternion targetRotation = Quaternion.Euler(0, angle, 0); // 생성할거에요 -> 목표 회전값(쿼터니언)을 + transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); // 회전시킬거에요 -> 현재 회전에서 목표 회전으로 부드럽게 } } \ No newline at end of file diff --git a/Assets/Scripts/Card_Scripts/RandomStatCardInstance.cs b/Assets/Scripts/Card_Scripts/RandomStatCardInstance.cs index f83d1c16..78ac3772 100644 --- a/Assets/Scripts/Card_Scripts/RandomStatCardInstance.cs +++ b/Assets/Scripts/Card_Scripts/RandomStatCardInstance.cs @@ -1,58 +1,60 @@ -using UnityEngine; +using UnityEngine; // Random.Range 같은 유니티 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class RandomStatCardInstance -{ - private RandomStatCardData data; - private StatType statType; - private int value; - private bool isConfirmed = false; - private Stats stats; +public class RandomStatCardInstance // 클래스를 선언할거에요 -> 랜덤 스탯 카드 1장의 동작/상태를 담당하는 RandomStatCardInstance를 +{ // 코드 블록을 시작할거에요 -> RandomStatCardInstance 범위를 - public RandomStatCardInstance(RandomStatCardData data, Stats stats) - { - this.data = data; - this.stats = stats; - } + private RandomStatCardData data; // 변수를 선언할거에요 -> 카드 데이터(가능 스탯/범위)를 담을 data를 + private StatType statType; // 변수를 선언할거에요 -> 이번에 뽑힌 스탯 종류를 담을 statType을 + private int value; // 변수를 선언할거에요 -> 이번에 뽑힌 수치를 담을 value를 + private bool isConfirmed = false; // 변수를 선언할거에요 -> 이미 적용 확정했는지 여부를 isConfirmed에(기본 false) + private Stats stats; // 변수를 선언할거에요 -> 실제 스탯을 적용할 대상(Stats)을 stats에 - public void RollPreview() - { - statType = data.possibleStats[Random.Range(0, data.possibleStats.Length)]; - value = Random.Range(data.minValue, data.maxValue + 1); - isConfirmed = false; - } + public RandomStatCardInstance(RandomStatCardData data, Stats stats) // 생성자를 선언할거에요 -> 데이터와 적용 대상을 받아 초기화하는 생성자를 + { // 코드 블록을 시작할거에요 -> 생성자 범위를 + this.data = data; // 값을 넣을거에요 -> 전달받은 data를 멤버 data에 저장 + this.stats = stats; // 값을 넣을거에요 -> 전달받은 stats를 멤버 stats에 저장 + } // 코드 블록을 끝낼거에요 -> 생성자를 - public string GetText() - { - string sign = value >= 0 ? "+" : ""; - return $"{statType} {sign}{value}"; - } + public void RollPreview() // 함수를 선언할거에요 -> 카드 미리보기(랜덤 결과) 뽑는 RollPreview를 + { // 코드 블록을 시작할거에요 -> RollPreview 범위를 + statType = data.possibleStats[Random.Range(0, data.possibleStats.Length)]; // 값을 뽑을거에요 -> 가능한 스탯 중 하나를 랜덤으로 + value = Random.Range(data.minValue, data.maxValue + 1); // 값을 뽑을거에요 -> min~max 범위에서 랜덤 수치를(+1은 int 상한 포함) + isConfirmed = false; // 상태를 바꿀거에요 -> 아직 확정 전(false)로 + } // 코드 블록을 끝낼거에요 -> RollPreview를 - public void Confirm() - { - if (isConfirmed) return; - ApplyStat(); - isConfirmed = true; - } + public string GetText() // 함수를 선언할거에요 -> UI에 보여줄 텍스트를 만드는 GetText를 + { // 코드 블록을 시작할거에요 -> GetText 범위를 + string sign = value >= 0 ? "+" : ""; // 값을 정할거에요 -> 양수면 + 표시, 음수면 빈 문자열로 + return $"{statType} {sign}{value}"; // 문자열을 반환할거에요 -> "스탯 +값" 형태로 + } // 코드 블록을 끝낼거에요 -> GetText를 - private void ApplyStat() - { - if (stats == null) return; + public void Confirm() // 함수를 선언할거에요 -> 선택 확정 시 실제 적용하는 Confirm을 + { // 코드 블록을 시작할거에요 -> Confirm 범위를 + if (isConfirmed) return; // 조건이 맞으면 종료할거에요 -> 이미 확정했으면 중복 적용 방지 + ApplyStat(); // 함수를 실행할거에요 -> 뽑힌 스탯을 실제로 적용 + isConfirmed = true; // 상태를 바꿀거에요 -> 이제 확정 완료(true)로 + } // 코드 블록을 끝낼거에요 -> Confirm을 - switch (statType) - { - case StatType.Health: - stats.AddMaxHealth(value); - break; + private void ApplyStat() // 함수를 선언할거에요 -> 스탯 적용 로직을 처리하는 ApplyStat을 + { // 코드 블록을 시작할거에요 -> ApplyStat 범위를 + if (stats == null) return; // 조건이 맞으면 종료할거에요 -> 적용 대상이 없으면 아무것도 안 함 - case StatType.Speed: - stats.AddMoveSpeed(value); - break; + switch (statType) // 분기할거에요 -> 어떤 스탯인지에 따라 + { // 코드 블록을 시작할거에요 -> switch 범위를 + case StatType.Health: // 조건을 처리할거에요 -> 체력 스탯이면 + stats.AddMaxHealth(value); // 함수를 실행할거에요 -> 최대 체력에 value만큼 더하기 + break; // 분기를 끝낼거에요 -> switch 탈출 - // ✨ [제거] case StatType.Strength 삭제됨 + case StatType.Speed: // 조건을 처리할거에요 -> 이동속도 스탯이면 + stats.AddMoveSpeed(value); // 함수를 실행할거에요 -> 이동속도에 value만큼 더하기 + break; // 분기를 끝낼거에요 -> switch 탈출 - case StatType.Damage: - stats.AddAttackDamage(value); - break; - } - } -} \ No newline at end of file + // ✨ [제거] case StatType.Strength 삭제됨 // 설명을 적을거에요 -> 힘 스탯은 더 이상 처리하지 않음 + + case StatType.Damage: // 조건을 처리할거에요 -> 공격력 스탯이면 + stats.AddAttackDamage(value); // 함수를 실행할거에요 -> 공격 데미지에 value만큼 더하기 + break; // 분기를 끝낼거에요 -> switch 탈출 + } // 코드 블록을 끝낼거에요 -> switch를 + } // 코드 블록을 끝낼거에요 -> ApplyStat을 + +} // 코드 블록을 끝낼거에요 -> RandomStatCardInstance를 \ No newline at end of file diff --git a/Assets/Scripts/Combat/Components/Health.cs b/Assets/Scripts/Combat/Components/Health.cs index 4397f9a5..903204f1 100644 --- a/Assets/Scripts/Combat/Components/Health.cs +++ b/Assets/Scripts/Combat/Components/Health.cs @@ -1,138 +1,135 @@ -using UnityEngine; -using System; -using System.Collections; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.Events; // 유니티 이벤트 기능을 사용할거에요 -> UnityEngine.Events를 +using System; // C# 기본 이벤트(Action)를 사용할거에요 -> System을 +using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를 -public class PlayerHealth : MonoBehaviour, IDamageable +/// +/// 플레이어의 체력, 피격, 무적, 사망을 관리합니다. +/// +public class PlayerHealth : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> PlayerHealth를 { - [Header("=== 참조 ===")] - [SerializeField] private Stats stats; - [SerializeField] private Animator animator; - [SerializeField] private PlayerAttack attackScript; + [Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 참조 === 를 + [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 스탯 스크립트를 playerStats에 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // 🔗 보스 패턴 스크립트 연결 (Inspector에서 할당) - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - [Header("🔗 보스 패턴 연결")] - public BossPatternPhases bossPattern; + [Header("=== 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 설정 === 을 + [SerializeField] private float invincibleDuration = 1.0f; // 변수를 선언할거에요 -> 무적 시간을 invincibleDuration에 + [SerializeField] private string hitAnimationName = "Player_Hit"; // 변수를 선언할거에요 -> 피격 애니메이션 이름을 hitAnimationName에 - public bool IsDead { get; private set; } - public bool isHit { get; private set; } - public bool isInvincible; // 대시 중 무적 플래그 + [Header("=== 유니티 이벤트 (인스펙터 연결용) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 유니티 이벤트 === 를 + public UnityEvent OnDieEvent; // 이벤트를 선언할거에요 -> 사망 시 발생할 UnityEvent인 OnDieEvent를 + public UnityEvent OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 시 발생할 OnHealthChanged를 - public event Action OnHitEvent, OnDead; - public event Action OnHealthChanged; + // ⭐ [이벤트] += 연산자 오류 방지를 위해 event Action 사용 + public event Action OnHitEvent; + public event Action OnHit; + public event Action OnDie; + public event Action OnDead; - private float _currentHealth; + // ⭐ [핵심 수정 1] 외부에서 수정 가능하도록 set 권한 개방 + public float CurrentHP { get; private set; } // (체력은 함부로 바꾸면 안되니 private set 유지) - private IEnumerator Start() + // "set 접근자에 액세스할 수 없습니다" 오류 해결 -> private 제거 + public bool isInvincible { get; set; } = false; + + public bool IsDead { get; private set; } = false; // 사망 여부는 내부에서만 관리 + + private Animator animator; // 변수를 선언할거에요 -> 애니메이터 컴포넌트를 담을 animator를 + + // ⭐ [핵심 수정 2] "보호 수준 때문에 액세스 불가" 오류 해결 -> public으로 변경 + public bool isHit = false; + + private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를 { - yield return null; - - if (stats != null) - { - _currentHealth = stats.MaxHealth; - OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth); - Debug.Log($"[UI Sync] 초기 체력 설정 완료: {_currentHealth}/{stats.MaxHealth}"); - } - if (animator == null) animator = GetComponent(); - if (attackScript == null) attackScript = GetComponent(); + animator = GetComponentInChildren(); // 컴포넌트를 가져올거에요 -> 자식의 애니메이터를 + if (playerStats == null) playerStats = GetComponent(); // 조건이 맞으면 가져올거에요 -> 스탯 컴포넌트가 비어있다면 내 몸에서 } - public void RefreshHealthUI() + private void Start() // 함수를 실행할거에요 -> 시작 Start를 { - if (stats != null) + if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 스탯이 있다면 { - _currentHealth = Mathf.Min(_currentHealth, stats.MaxHealth); - OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth); + CurrentHP = playerStats.MaxHealth; // 값을 초기화할거에요 -> 현재 체력을 최대 체력으로 + } + else // 조건이 틀리면 실행할거에요 -> 스탯이 없다면 + { + CurrentHP = 100f; // 값을 넣을거에요 -> 기본 체력 100으로 + Debug.LogWarning("⚠️ Stats 컴포넌트가 없습니다! 기본 체력 100 적용."); // 경고를 출력할거에요 + } + + RefreshHealthUI(); // 함수를 실행할거에요 -> 초기 체력 UI 갱신을 + } + + public void TakeDamage(float damage) // 함수를 선언할거에요 -> 외부에서 호출할 피격 함수 TakeDamage를 + { + if (IsDead || isInvincible) return; // 조건이 맞으면 중단할거에요 -> 죽었거나 무적이라면 + + // 실제 데미지 적용 + CurrentHP -= damage; // 값을 뺄거에요 -> 체력에서 데미지를 + if (CurrentHP < 0) CurrentHP = 0; // 조건이 맞으면 보정할거에요 -> 체력이 음수가 안 되게 0으로 + + Debug.Log($"💥 피격! 데미지: {damage}, 남은 체력: {CurrentHP}"); // 로그를 출력할거에요 -> 피격 정보를 + + RefreshHealthUI(); // 함수를 실행할거에요 -> 체력바 갱신을 + + if (CurrentHP <= 0) // 조건이 맞으면 실행할거에요 -> 체력이 바닥났다면 + { + Die(); // 함수를 실행할거에요 -> 사망 처리 Die를 + } + else // 조건이 틀리면 실행할거에요 -> 아직 살았다면 + { + StartCoroutine(HitRoutine()); // 코루틴을 실행할거에요 -> 피격 무적/경직 처리를 + + // 모든 이벤트 호출 + OnHit?.Invoke(); + OnHitEvent?.Invoke(); } } - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // ⭐ 데미지를 받는 함수 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - public void TakeDamage(float amount) + private void Die() // 함수를 선언할거에요 -> 사망 처리 Die를 { - // 무적이거나 이미 죽었으면 데미지 무시 - if (isInvincible || IsDead) return; + if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면 - _currentHealth = Mathf.Max(0, _currentHealth - amount); - OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth); + IsDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참으로 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // 🔗 NEW: 보스 패턴 시스템에 "피격당함"을 알림 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - if (bossPattern != null) - { - // BossPatternPhases 스크립트의 OnPlayerHit() 함수 호출 - // 이 함수 안에서 "패턴 진행 중인지" 체크하고 XP 감점 처리 - bossPattern.OnPlayerHit(); - } + OnDieEvent?.Invoke(); // 유니티 이벤트 + OnDie?.Invoke(); // C# 이벤트 + OnDead?.Invoke(); // C# 이벤트 (호환용) - // ⭐ 피격 시 트리거 및 상태 리셋 함수 호출 - OnHit(); - - OnHitEvent?.Invoke(); // 이벤트 발생 - - if (!IsDead) StartHit(); - if (_currentHealth <= 0) Die(); + Debug.Log("💀 플레이어 사망!"); // 로그를 출력할거에요 -> 사망 메시지를 } - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // ⭐ 피격 시 공격 상태 리셋 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - public void OnHit() + public void RefreshHealthUI() // 함수를 선언할거에요 -> UI를 갱신하는 RefreshHealthUI를 { + float ratio = (playerStats != null && playerStats.MaxHealth > 0) ? CurrentHP / playerStats.MaxHealth : 0; // 비율을 계산할거에요 -> 현재 체력 / 최대 체력으로 + OnHealthChanged?.Invoke(ratio); // 이벤트를 실행할거에요 -> UI 슬라이더 값을 업데이트하도록 + } + + private IEnumerator HitRoutine() // 코루틴 함수를 선언할거에요 -> 피격 경직 및 무적 처리 HitRoutine을 + { + isHit = true; // 상태를 바꿀거에요 -> 경직 상태를 참으로 + isInvincible = true; // 상태를 바꿀거에요 -> 무적 상태를 참으로 + if (animator != null) { - // 1. 남아있는 공격 트리거를 강제로 꺼버림 (유령 공격 방지) - animator.ResetTrigger("Attack"); - - // 2. 다른 공격(예: 투척) 트리거가 있다면 그것도 리셋 - animator.ResetTrigger("Throw"); + animator.Play(hitAnimationName, 0, 0f); // 재생할거에요 -> 설정된 피격 애니메이션을 } - // 3. PlayerAttack 스크립트의 공격 중인 상태 플래그도 강제로 꺼줌 - if (attackScript != null) - { - attackScript.IsAttacking = false; - } + yield return new WaitForSeconds(0.3f); // 기다릴거에요 -> 경직 시간(0.3초)만큼 + isHit = false; // 상태를 바꿀거에요 -> 경직 해제 - Debug.Log("[Combat] 피격으로 인해 공격 예약 및 상태가 초기화되었습니다."); + yield return new WaitForSeconds(invincibleDuration - 0.3f); // 기다릴거에요 -> 남은 무적 시간만큼 + isInvincible = false; // 상태를 바꿀거에요 -> 무적 해제 } - private void StartHit() + public void Heal(float amount) // 함수를 선언할거에요 -> 체력을 회복하는 Heal을 { - isHit = true; - // 인스펙터에 적힌 피격 애니메이션 이름(HitAnime) 재생 - if (animator != null) animator.Play("HitAnime", 0, 0f); + if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 죽은 상태라면 - CancelInvoke(nameof(OnHitEnd)); - Invoke(nameof(OnHitEnd), 0.25f); - } + CurrentHP += amount; // 값을 더할거에요 -> 체력에 회복량을 + if (playerStats != null && CurrentHP > playerStats.MaxHealth) // 조건이 맞으면 실행할거에요 -> 최대 체력을 넘었다면 + CurrentHP = playerStats.MaxHealth; // 값을 제한할거에요 -> 최대 체력으로 - public void OnHitEnd() { isHit = false; } - - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // ⭐ 사망 처리 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - private void Die() - { - IsDead = true; - Cursor.visible = true; - Cursor.lockState = CursorLockMode.None; - OnDead?.Invoke(); - - // ★ ★ ★ NEW: 사망 시 임시 XP를 영구 XP로 전환 ★ ★ ★ - if (ObsessionSystem.instance != null) - { - ObsessionSystem.instance.OnDeathConvertXP(); - } - } - - public void Heal(float amount) - { - if (IsDead) return; - _currentHealth = Mathf.Min(_currentHealth + amount, stats.MaxHealth); - OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth); + RefreshHealthUI(); // 함수를 실행할거에요 -> UI 갱신을 + Debug.Log($"💚 체력 회복: {amount}, 현재: {CurrentHP}"); // 로그를 출력할거에요 -> 회복 정보를 } } \ No newline at end of file diff --git a/Assets/Scripts/Combat/Debug/DamageBot.cs b/Assets/Scripts/Combat/Debug/DamageBot.cs index a510ab43..fc912fdc 100644 --- a/Assets/Scripts/Combat/Debug/DamageBot.cs +++ b/Assets/Scripts/Combat/Debug/DamageBot.cs @@ -1,14 +1,22 @@ -using UnityEngine; -public class DamageBot : MonoBehaviour -{ - [SerializeField] private float damageAmount = 10f, damageInterval = 1.0f; - private float timer; - private void OnTriggerStay(Collider other) - { - if (other.TryGetComponent(out var target)) - { - timer += Time.deltaTime; - if (timer >= damageInterval) { target.TakeDamage(damageAmount); timer = 0f; } - } - } -} \ No newline at end of file +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 + +public class DamageBot : MonoBehaviour // 클래스를 선언할거에요 -> 트리거 안에 있는 대상에게 주기적으로 데미지를 주는 DamageBot을 +{ // 코드 블록을 시작할거에요 -> DamageBot 범위를 + + [SerializeField] private float damageAmount = 10f, damageInterval = 1.0f; // 변수를 선언할거에요 -> 데미지량(10)과 데미지 간격(1초)을 + private float timer; // 변수를 선언할거에요 -> 시간 누적용 타이머를 timer에 + + private void OnTriggerStay(Collider other) // 함수를 선언할거에요 -> 트리거 안에 머무는 동안 계속 호출되는 OnTriggerStay를 + { // 코드 블록을 시작할거에요 -> OnTriggerStay 범위를 + if (other.TryGetComponent(out var target)) // 조건을 검사할거에요 -> 대상이 IDamageable을 가지고 있는지 + { // 코드 블록을 시작할거에요 -> 데미지 가능 대상 처리 + timer += Time.deltaTime; // 값을 더할거에요 -> 지난 프레임 시간만큼 타이머 누적 + if (timer >= damageInterval) // 조건을 검사할거에요 -> 누적 시간이 간격을 넘었는지 + { // 코드 블록을 시작할거에요 -> 데미지 적용 처리 + target.TakeDamage(damageAmount); // 함수를 실행할거에요 -> 대상에게 damageAmount만큼 데미지 주기 + timer = 0f; // 값을 초기화할거에요 -> 다음 틱을 위해 타이머를 0으로 + } // 코드 블록을 끝낼거에요 -> 데미지 적용 처리 + } // 코드 블록을 끝낼거에요 -> 데미지 가능 대상 처리 + } // 코드 블록을 끝낼거에요 -> OnTriggerStay를 + +} // 코드 블록을 끝낼거에요 -> DamageBot을 \ No newline at end of file diff --git a/Assets/Scripts/Combat/Interfaces/IDamageable.cs b/Assets/Scripts/Combat/Interfaces/IDamageable.cs index 1aa2caad..322338a1 100644 --- a/Assets/Scripts/Combat/Interfaces/IDamageable.cs +++ b/Assets/Scripts/Combat/Interfaces/IDamageable.cs @@ -1,6 +1,8 @@ -public interface IDamageable -{ - // ޾ ԼԴϴ. - // (damage) ڷ ޽ϴ. - void TakeDamage(float damage); -} \ No newline at end of file +public interface IDamageable // 인터페이스를 선언할거에요 -> 데미지를 받을 수 있는 대상의 규칙(IDamageable)을 +{ // 코드 블록을 시작할거에요 -> IDamageable 범위를 + + // 공격을 받았을 때 실행될 함수입니다. // 설명을 적을거에요 -> 데미지 받을 때 호출되는 함수임을 + // 데미지 양(damage)을 인자로 받습니다. // 설명을 적을거에요 -> 들어오는 데미지 값이 damage라는 걸 + void TakeDamage(float damage); // 함수 원형을 정의할거에요 -> float damage를 받아 데미지 처리하는 TakeDamage를 + +} // 코드 블록을 끝낼거에요 -> IDamageable을 \ No newline at end of file diff --git a/Assets/Scripts/Combat/WePonHitBox.cs b/Assets/Scripts/Combat/WePonHitBox.cs index 07665e1a..c733014c 100644 --- a/Assets/Scripts/Combat/WePonHitBox.cs +++ b/Assets/Scripts/Combat/WePonHitBox.cs @@ -1,41 +1,43 @@ -using UnityEngine; -using System.Collections.Generic; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 +using System.Collections.Generic; // List를 쓰기 위해 불러올거에요 -> System.Collections.Generic을 -public class WeaponHitBox : MonoBehaviour -{ - private float _damage; - private bool _isActive = false; - private List _hitTargets = new List(); +public class WeaponHitBox : MonoBehaviour // 클래스를 선언할거에요 -> 무기 히트박스(트리거)로 타격을 처리하는 WeaponHitBox를 +{ // 코드 블록을 시작할거에요 -> WeaponHitBox 범위를 - public void EnableHitBox(float damage) - { - _damage = damage; - _isActive = true; - _hitTargets.Clear(); - gameObject.SetActive(true); - } + private float _damage; // 변수를 선언할거에요 -> 이번 공격에서 적용할 데미지를 _damage에 + private bool _isActive = false; // 변수를 선언할거에요 -> 히트박스 활성 여부를 _isActive에(기본 false) + private List _hitTargets = new List(); // 리스트를 만들거에요 -> 이미 때린 대상 저장용 _hitTargets를 - public void DisableHitBox() - { - _isActive = false; - gameObject.SetActive(false); - } + public void EnableHitBox(float damage) // 함수를 선언할거에요 -> 히트박스를 켜고 데미지를 세팅하는 EnableHitBox를 + { // 코드 블록을 시작할거에요 -> EnableHitBox 범위를 + _damage = damage; // 값을 넣을거에요 -> 받은 damage를 _damage로 설정 + _isActive = true; // 상태를 바꿀거에요 -> 히트박스를 활성 상태(true)로 + _hitTargets.Clear(); // 리스트를 비울거에요 -> 이전 공격에서 맞춘 기록을 초기화 + gameObject.SetActive(true); // 오브젝트를 켤거에요 -> 히트박스 트리거 오브젝트 활성화 + } // 코드 블록을 끝낼거에요 -> EnableHitBox를 - private void OnTriggerEnter(Collider other) - { - if (!_isActive || other.CompareTag("Player")) return; + public void DisableHitBox() // 함수를 선언할거에요 -> 히트박스를 끄는 DisableHitBox를 + { // 코드 블록을 시작할거에요 -> DisableHitBox 범위를 + _isActive = false; // 상태를 바꿀거에요 -> 히트박스를 비활성(false)로 + gameObject.SetActive(false); // 오브젝트를 끌거에요 -> 히트박스 트리거 오브젝트 비활성화 + } // 코드 블록을 끝낼거에요 -> DisableHitBox를 - // ⭐ [핵심] 몬스터의 감지 영역(Trigger)은 무시하고 '진짜 몸통'만 타격! - if (other.isTrigger) return; + private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 뭔가 들어오면 호출되는 OnTriggerEnter를 + { // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를 + if (!_isActive || other.CompareTag("Player")) return; // 조건이 맞으면 종료할거에요 -> 비활성이거나 플레이어면 무시 - if (other.TryGetComponent(out var target)) - { - if (!_hitTargets.Contains(target)) - { - target.TakeDamage(_damage); - _hitTargets.Add(target); - Debug.Log($"[Hit] {other.name}의 몸통을 정확히 타격!"); - } - } - } -} \ No newline at end of file + // ⭐ [핵심] 몬스터의 감지 영역(Trigger)은 무시하고 '진짜 몸통'만 타격! // 설명을 적을거에요 -> 트리거 콜라이더는 공격 대상으로 안 잡겠다는 뜻 + if (other.isTrigger) return; // 조건이 맞으면 종료할거에요 -> 상대 콜라이더가 트리거면 무시 + + if (other.TryGetComponent(out var target)) // 조건을 검사할거에요 -> IDamageable을 가진 대상인지 + { // 코드 블록을 시작할거에요 -> 데미지 처리 + if (!_hitTargets.Contains(target)) // 조건을 검사할거에요 -> 이번 공격에서 아직 안 때린 대상인지 + { // 코드 블록을 시작할거에요 -> 1회 타격 처리 + target.TakeDamage(_damage); // 함수를 실행할거에요 -> 대상에게 _damage만큼 데미지 주기 + _hitTargets.Add(target); // 리스트에 추가할거에요 -> 중복 타격 방지를 위해 대상 기록 + Debug.Log($"[Hit] {other.name}의 몸통을 정확히 타격!"); // 로그를 찍을거에요 -> 타격 확인 + } // 코드 블록을 끝낼거에요 -> 1회 타격 처리 + } // 코드 블록을 끝낼거에요 -> 데미지 처리 + } // 코드 블록을 끝낼거에요 -> OnTriggerEnter를 + +} // 코드 블록을 끝낼거에요 -> WeaponHitBox를 \ No newline at end of file diff --git a/Assets/Scripts/Combat/WeponConfig.cs b/Assets/Scripts/Combat/WeponConfig.cs index cdd0949d..d5533ba1 100644 --- a/Assets/Scripts/Combat/WeponConfig.cs +++ b/Assets/Scripts/Combat/WeponConfig.cs @@ -1,39 +1,41 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능 + ScriptableObject를 쓰기 위해 불러올거에요 -> UnityEngine를 -[CreateAssetMenu(fileName = "NewWeapon", menuName = "Weapons/Config")] -public class WeaponConfig : ScriptableObject -{ - [Header("기본 능력치")] - [SerializeField] private float baseDamage = 10f; - [SerializeField] private int requiredStrength = 5; +[CreateAssetMenu(fileName = "NewWeapon", menuName = "Weapons/Config")] // 에셋 생성 메뉴를 추가할거에요 -> Create > Weapons > Config로 WeaponConfig를 만들 수 있게 +public class WeaponConfig : ScriptableObject // 클래스를 선언할거에요 -> 무기 설정 데이터를 담는 ScriptableObject인 WeaponConfig를 +{ // 코드 블록을 시작할거에요 -> WeaponConfig 범위를 - public float BaseDamage => baseDamage; - public int RequiredStrength => requiredStrength; + [Header("기본 능력치")] // 인스펙터에 제목을 표시할거에요 -> 기본 능력치 섹션을 + [SerializeField] private float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 데미지를 10으로 baseDamage에 + [SerializeField] private int requiredStrength = 5; // 변수를 선언할거에요 -> 요구 힘 스탯을 5로 requiredStrength에 - [Header("애니메이션 설정")] - [SerializeField] private string normalAttackTrigger = "Attack"; - [SerializeField] private string chargingBool = "IsCharging"; - [SerializeField] private string throwTrigger = "Throw"; + public float BaseDamage => baseDamage; // 프로퍼티를 선언할거에요 -> baseDamage를 읽기 전용으로 노출 + public int RequiredStrength => requiredStrength; // 프로퍼티를 선언할거에요 -> requiredStrength를 읽기 전용으로 노출 - public string NormalAttackTrigger => normalAttackTrigger; - public string ChargingBool => chargingBool; - public string ThrowTrigger => throwTrigger; + [Header("애니메이션 설정")] // 인스펙터에 제목을 표시할거에요 -> 애니메이션 설정 섹션을 + [SerializeField] private string normalAttackTrigger = "Attack"; // 변수를 선언할거에요 -> 일반 공격 트리거 이름을 normalAttackTrigger에 + [SerializeField] private string chargingBool = "IsCharging"; // 변수를 선언할거에요 -> 차징 여부 bool 파라미터 이름을 chargingBool에 + [SerializeField] private string throwTrigger = "Throw"; // 변수를 선언할거에요 -> 던지기 트리거 이름을 throwTrigger에 - [Header("차징 단계별 설정 (인스펙터 수정)")] - [SerializeField] private float forceLv1 = 10f; - [SerializeField] private float spreadLv1 = 25f; - [SerializeField] private float forceLv2 = 18f; - [SerializeField] private float spreadLv2 = 8f; - [SerializeField] private float forceLv3 = 28f; - // Lv3 Spread는 인스펙터에서 보이지 않게 하거나, 로직에서 무시하도록 처리합니다. + public string NormalAttackTrigger => normalAttackTrigger; // 프로퍼티를 선언할거에요 -> 일반 공격 트리거 문자열을 읽기 전용으로 노출 + public string ChargingBool => chargingBool; // 프로퍼티를 선언할거에요 -> 차징 bool 문자열을 읽기 전용으로 노출 + public string ThrowTrigger => throwTrigger; // 프로퍼티를 선언할거에요 -> 던지기 트리거 문자열을 읽기 전용으로 노출 - // ⭐ 힘(Force)은 단계별로 가져옵니다. - public float GetForce(int lv) => lv == 3 ? forceLv3 : (lv == 2 ? forceLv2 : forceLv1); + [Header("차징 단계별 설정 (인스펙터 수정)")] // 인스펙터에 제목을 표시할거에요 -> 차징 단계별 설정 섹션을 + [SerializeField] private float forceLv1 = 10f; // 변수를 선언할거에요 -> 1단계 힘(발사력)을 10으로 forceLv1에 + [SerializeField] private float spreadLv1 = 25f; // 변수를 선언할거에요 -> 1단계 퍼짐(정확도)을 25로 spreadLv1에 + [SerializeField] private float forceLv2 = 18f; // 변수를 선언할거에요 -> 2단계 힘(발사력)을 18로 forceLv2에 + [SerializeField] private float spreadLv2 = 8f; // 변수를 선언할거에요 -> 2단계 퍼짐(정확도)을 8로 spreadLv2에 + [SerializeField] private float forceLv3 = 28f; // 변수를 선언할거에요 -> 3단계 힘(발사력)을 28로 forceLv3에 + // Lv3 Spread는 인스펙터에서 보이지 않게 하거나, 로직에서 무시하도록 처리합니다. // 설명을 적을거에요 -> 3단계는 퍼짐을 안 쓰는 설계임을 - // ⭐ [수정] 정확도(Spread) 로직: 3단계는 무조건 0(직선) 반환! - public float GetSpread(int lv) - { - if (lv == 3) return 0f; // 풀차징은 무조건 일자로 날아감 - return (lv == 2) ? spreadLv2 : spreadLv1; - } -} \ No newline at end of file + // ⭐ 힘(Force)은 단계별로 가져옵니다. // 설명을 적을거에요 -> 단계에 맞는 force를 반환함을 + public float GetForce(int lv) => lv == 3 ? forceLv3 : (lv == 2 ? forceLv2 : forceLv1); // 값을 반환할거에요 -> lv에 따라 forceLv1/2/3 중 하나를 + + // ⭐ [수정] 정확도(Spread) 로직: 3단계는 무조건 0(직선) 반환! // 설명을 적을거에요 -> 풀차징은 직선 고정임을 + public float GetSpread(int lv) // 함수를 선언할거에요 -> 단계별 spread를 가져오는 GetSpread를 + { // 코드 블록을 시작할거에요 -> GetSpread 범위를 + if (lv == 3) return 0f; // 조건이 맞으면 반환할거에요 -> 3단계면 퍼짐 0(직선) + return (lv == 2) ? spreadLv2 : spreadLv1; // 값을 반환할거에요 -> 2단계면 spreadLv2, 아니면 spreadLv1 + } // 코드 블록을 끝낼거에요 -> GetSpread를 + +} // 코드 블록을 끝낼거에요 -> WeaponConfig를 \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/ChargeMonster.cs b/Assets/Scripts/Enemy/AI/ChargeMonster.cs index d4a32f69..03a484c5 100644 --- a/Assets/Scripts/Enemy/AI/ChargeMonster.cs +++ b/Assets/Scripts/Enemy/AI/ChargeMonster.cs @@ -1,211 +1,211 @@ -using UnityEngine; -using UnityEngine.AI; -using System.Collections; -using System.Collections.Generic; // ✅ 리스트 사용 필수 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 /// /// 돌진 공격 몬스터 /// -public class ChargeMonster : MonsterClass +public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ChargeMonster를 { - [Header("=== 돌진 공격 설정 ===")] - [SerializeField] private float chargeSpeed = 15f; - [SerializeField] private float chargeRange = 10f; - [SerializeField] private float chargeDuration = 2f; - [SerializeField] private float chargeDelay = 3f; - [SerializeField] private float prepareTime = 0.5f; + [Header("=== 돌진 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 돌진 공격 설정 === 을 + [SerializeField] private float chargeSpeed = 15f; // 변수를 선언할거에요 -> 돌진 속도(15.0)를 chargeSpeed에 + [SerializeField] private float chargeRange = 10f; // 변수를 선언할거에요 -> 돌진 시작 거리(10.0)를 chargeRange에 + [SerializeField] private float chargeDuration = 2f; // 변수를 선언할거에요 -> 돌진 지속 시간(2.0초)을 chargeDuration에 + [SerializeField] private float chargeDelay = 3f; // 변수를 선언할거에요 -> 돌진 쿨타임(3.0초)을 chargeDelay에 + [SerializeField] private float prepareTime = 0.5f; // 변수를 선언할거에요 -> 돌진 준비 시간(0.5초)을 prepareTime에 // 🎁 [추가] 드랍 아이템 리스트 - [Header("=== 드랍 아이템 설정 ===")] - [Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")] - [SerializeField] private List dropItemPrefabs; - [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] - [Range(0, 100)][SerializeField] private float dropChance = 30f; + [Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을 + [Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private List dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍 아이템 프리팹 목록을 dropItemPrefabs에 + [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에 - private float lastChargeTime; - private bool isCharging = false; - private bool isPreparing = false; - private Vector3 chargeDirection; + private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을 lastChargeTime에 + private bool isCharging = false; // 변수를 선언할거에요 -> 돌진 중 여부를 isCharging에 + private bool isPreparing = false; // 변수를 선언할거에요 -> 준비 중 여부를 isPreparing에 + private Vector3 chargeDirection; // 변수를 선언할거에요 -> 돌진 방향 벡터를 chargeDirection에 - [Header("공격 / 이동 애니메이션")] - [SerializeField] private string chargeAnimation = "Monster_Charge"; - [SerializeField] private string prepareAnimation = "Monster_ChargePrepare"; - [SerializeField] private string Monster_Walk = "Monster_Walk"; + [Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을 + [SerializeField] private string chargeAnimation = "Monster_Charge"; // 변수를 선언할거에요 -> 돌진 애니메이션 이름을 chargeAnimation에 + [SerializeField] private string prepareAnimation = "Monster_ChargePrepare"; // 변수를 선언할거에요 -> 준비 애니메이션 이름을 prepareAnimation에 + [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에 - [Header("AI 상세 설정")] - [SerializeField] private float patrolRadius = 5f; - [SerializeField] private float patrolInterval = 2f; + [Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을 + [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 + [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에 - private float nextPatrolTime; - private bool isPlayerInZone; - private Rigidbody _rigidbody; + private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 + private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에 + private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트를 담을 _rigidbody를 - protected override void Awake() + protected override void Awake() // 함수를 덮어씌워 실행할거에요 -> Awake 초기화 로직을 { - base.Awake(); - _rigidbody = GetComponent(); - if (_rigidbody == null) _rigidbody = gameObject.AddComponent(); - _rigidbody.isKinematic = true; + base.Awake(); // 부모의 함수를 실행할거에요 -> MonsterClass의 Awake를 + _rigidbody = GetComponent(); // 컴포넌트를 가져올거에요 -> Rigidbody를 + if (_rigidbody == null) _rigidbody = gameObject.AddComponent(); // 조건이 맞으면 실행할거에요 -> 없으면 새로 추가해서 _rigidbody에 + _rigidbody.isKinematic = true; // 설정을 바꿀거에요 -> 물리 연산을 꺼두기(Kinematic)로 } - protected override void Init() + protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기 설정을 Init에서 { - if (agent != null) agent.stoppingDistance = 1f; - if (animator != null) animator.applyRootMotion = false; + if (agent != null) agent.stoppingDistance = 1f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 1.0으로 + if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로 } - protected override void ExecuteAILogic() + protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { - if (isHit || isCharging || isPreparing) return; + if (isHit || isCharging || isPreparing) return; // 조건이 맞으면 중단할거에요 -> 피격, 돌진, 준비 중이라면 - float distance = Vector3.Distance(transform.position, playerTransform.position); + float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 - if (isPlayerInZone || distance <= chargeRange * 1.5f) + if (isPlayerInZone || distance <= chargeRange * 1.5f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면 { - HandleChargeCombat(distance); + HandleChargeCombat(distance); // 함수를 실행할거에요 -> 전투 로직인 HandleChargeCombat을 } - else + else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 { - Patrol(); + Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을 } - UpdateMovementAnimation(); + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateMovementAnimation을 } - void HandleChargeCombat(float distance) + void HandleChargeCombat(float distance) // 함수를 선언할거에요 -> 거리 기반 전투 판단 로직 HandleChargeCombat을 { - if (distance <= chargeRange && Time.time >= lastChargeTime + chargeDelay) + if (distance <= chargeRange && Time.time >= lastChargeTime + chargeDelay) // 조건이 맞으면 실행할거에요 -> 사거리 안이고 쿨타임이 지났다면 { - StartCoroutine(PrepareCharge()); + StartCoroutine(PrepareCharge()); // 코루틴을 실행할거에요 -> 돌진 준비인 PrepareCharge를 } - else if (!isCharging && !isPreparing) + else if (!isCharging && !isPreparing) // 조건이 맞으면 실행할거에요 -> 돌진이나 준비 중이 아니라면 (추격) { - if (agent.isOnNavMesh) + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { - if (agent.isStopped) agent.isStopped = false; - agent.SetDestination(playerTransform.position); + if (agent.isStopped) agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고 + agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 } } } - IEnumerator PrepareCharge() + IEnumerator PrepareCharge() // 코루틴 함수를 선언할거에요 -> 돌진 준비 과정인 PrepareCharge를 { - isPreparing = true; - if (agent.isOnNavMesh) + isPreparing = true; // 상태를 바꿀거에요 -> 준비 중 상태를 참(true)으로 + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { - agent.isStopped = true; - agent.ResetPath(); - agent.velocity = Vector3.zero; + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 + agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 초기화하라고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } - chargeDirection = (playerTransform.position - transform.position).normalized; - chargeDirection.y = 0; - if (chargeDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(chargeDirection); + chargeDirection = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을 + chargeDirection.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게 y를 0으로 + if (chargeDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(chargeDirection); // 회전시킬거에요 -> 돌진 방향을 바라보게 - if (!string.IsNullOrEmpty(prepareAnimation)) animator.Play(prepareAnimation, 0, 0f); + if (!string.IsNullOrEmpty(prepareAnimation)) animator.Play(prepareAnimation, 0, 0f); // 재생할거에요 -> 준비 애니메이션을 - Debug.Log("[ChargeMonster] 돌진 준비..."); - yield return new WaitForSeconds(prepareTime); + Debug.Log("[ChargeMonster] 돌진 준비..."); // 로그를 출력할거에요 -> 준비 메시지를 + yield return new WaitForSeconds(prepareTime); // 기다릴거에요 -> 준비 시간만큼 - StartCoroutine(Charge()); + StartCoroutine(Charge()); // 코루틴을 실행할거에요 -> 실제 돌진인 Charge를 } - IEnumerator Charge() + IEnumerator Charge() // 코루틴 함수를 선언할거에요 -> 실제 돌진 동작인 Charge를 { - isPreparing = false; - isCharging = true; - lastChargeTime = Time.time; + isPreparing = false; // 상태를 바꿀거에요 -> 준비 상태를 거짓(false)으로 + isCharging = true; // 상태를 바꿀거에요 -> 돌진 중 상태를 참(true)으로 + lastChargeTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 돌진 시간으로 - if (agent != null) agent.enabled = false; - if (_rigidbody != null) _rigidbody.isKinematic = false; + if (agent != null) agent.enabled = false; // 기능을 끌거에요 -> 길찾기 에이전트를 (물리 이동을 위해) + if (_rigidbody != null) _rigidbody.isKinematic = false; // 기능을 켤거에요 -> 물리 연산을 (충돌 감지를 위해) - animator.Play(chargeAnimation, 0, 0f); - Debug.Log("[ChargeMonster] 돌진 시작!"); + animator.Play(chargeAnimation, 0, 0f); // 재생할거에요 -> 돌진 애니메이션을 + Debug.Log("[ChargeMonster] 돌진 시작!"); // 로그를 출력할거에요 -> 돌진 시작 메시지를 - float chargeStartTime = Time.time; + float chargeStartTime = Time.time; // 값을 저장할거에요 -> 돌진 시작 시간을 - while (Time.time < chargeStartTime + chargeDuration) + while (Time.time < chargeStartTime + chargeDuration) // 반복할거에요 -> 지속 시간이 끝날 때까지 { - if (_rigidbody != null) - _rigidbody.velocity = chargeDirection * chargeSpeed; - else - transform.position += chargeDirection * chargeSpeed * Time.deltaTime; - yield return null; + if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면 + _rigidbody.velocity = chargeDirection * chargeSpeed; // 값을 넣을거에요 -> 속도를 돌진 방향과 속도로 + else // 조건이 틀리면 실행할거에요 -> 리지드바디가 없다면 (비상용) + transform.position += chargeDirection * chargeSpeed * Time.deltaTime; // 이동시킬거에요 -> 위치를 직접 수정해서 + yield return null; // 대기할거에요 -> 다음 프레임까지 } - if (_rigidbody != null) + if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면 { - _rigidbody.velocity = Vector3.zero; - _rigidbody.isKinematic = true; + _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 + _rigidbody.isKinematic = true; // 기능을 끌거에요 -> 물리 연산을 (다시 NavMesh 이동을 위해) } - if (agent != null) + if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 { - agent.enabled = true; - agent.ResetPath(); - agent.velocity = Vector3.zero; + agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를 + agent.ResetPath(); // 초기화할거에요 -> 경로를 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 내부 속도를 0으로 } - isCharging = false; - Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작"); + isCharging = false; // 상태를 바꿀거에요 -> 돌진 중 상태를 거짓(false)으로 + Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작"); // 로그를 출력할거에요 -> 종료 메시지를 } - void UpdateMovementAnimation() + void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을 { - if (isCharging || isPreparing || isHit || isResting) return; - if (agent.enabled && agent.velocity.magnitude > 0.1f) - animator.Play(Monster_Walk); - else - animator.Play(Monster_Idle); + if (isCharging || isPreparing || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 + if (agent.enabled && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면 + animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을 + else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면 + animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을 } - void Patrol() + void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을 { - if (Time.time < nextPatrolTime) return; - Vector3 randomPoint = transform.position + new Vector3( + if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면 + Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을 Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius) ); - if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) - if (agent.isOnNavMesh) agent.SetDestination(hit.position); + if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 NavMesh 위치라면 + if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그곳으로 이동하라고 - nextPatrolTime = Time.time + patrolInterval; + nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을 } - private void OnCollisionEnter(Collision collision) + private void OnCollisionEnter(Collision collision) // 함수를 실행할거에요 -> 물리 충돌이 발생했을 때 OnCollisionEnter를 { - if (!isCharging) return; - if (collision.gameObject.CompareTag("Player")) + if (!isCharging) return; // 조건이 맞으면 중단할거에요 -> 돌진 중이 아니라면 + if (collision.gameObject.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌한 대상이 플레이어라면 { - if (collision.gameObject.TryGetComponent(out var playerHealth)) + if (collision.gameObject.TryGetComponent(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를 { - if (!playerHealth.isInvincible) - playerHealth.TakeDamage(attackDamage); + if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면 + playerHealth.TakeDamage(attackDamage); // 함수를 실행할거에요 -> 데미지를 입히는 TakeDamage를 } } } // ☠️ [추가] 죽을 때 아이템 드랍 - protected override void OnDie() + protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 아이템 드랍 로직인 OnDie를 { - if (_rigidbody != null) _rigidbody.velocity = Vector3.zero; + if (_rigidbody != null) _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 죽을 때 미끄러지지 않게 속도를 0으로 - if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) + if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 아이템 리스트가 있다면 { - float randomValue = Random.Range(0f, 100f); - if (randomValue <= dropChance) + float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0~100 사이 랜덤값을 + if (randomValue <= dropChance) // 조건이 맞으면 실행할거에요 -> 확률에 당첨되었다면 { - int randomIndex = Random.Range(0, dropItemPrefabs.Count); - if (dropItemPrefabs[randomIndex] != null) + int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 랜덤 인덱스를 + if (dropItemPrefabs[randomIndex] != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면 { - Instantiate(dropItemPrefabs[randomIndex], transform.position + Vector3.up * 0.5f, Quaternion.identity); + Instantiate(dropItemPrefabs[randomIndex], transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에 } } } } - private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } - private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } + private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 감지 시 참(true)으로 + private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어가 나가면 거짓(false)으로 } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/ExplodeMonster.cs b/Assets/Scripts/Enemy/AI/ExplodeMonster.cs index d582d4b0..7a2bbdd2 100644 --- a/Assets/Scripts/Enemy/AI/ExplodeMonster.cs +++ b/Assets/Scripts/Enemy/AI/ExplodeMonster.cs @@ -1,211 +1,211 @@ -using UnityEngine; -using UnityEngine.AI; -using System.Collections; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 /// /// 자폭 몬스터 (Kamikaze) /// - 플레이어에게 전력 질주 /// - 범위 내 진입 시 제자리에서 카운트다운 후 폭발 /// -public class ExplodeMonster : MonsterClass +public class ExplodeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ExplodeMonster를 { - [Header("=== 자폭 설정 ===")] - [SerializeField] private float explodeRange = 4f; // 데미지 입히는 범위 (폭발 반경) - [SerializeField] private float triggerRange = 2.5f; // 폭발을 시작하는 거리 (이 안에 들어오면 카운트다운) - [SerializeField] private float fuseTime = 1.5f; // 지연 시간 (도망갈 기회 줌) - [SerializeField] private float explosionDamage = 50f; // 폭발 데미지 + [Header("=== 자폭 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 자폭 설정 === 을 + [SerializeField] private float explodeRange = 4f; // 변수를 선언할거에요 -> 폭발 데미지 반경(4.0)을 explodeRange에 + [SerializeField] private float triggerRange = 2.5f; // 변수를 선언할거에요 -> 자폭 시퀀스 시작 거리(2.5)를 triggerRange에 + [SerializeField] private float fuseTime = 1.5f; // 변수를 선언할거에요 -> 폭발 지연 시간(1.5초)을 fuseTime에 + [SerializeField] private float explosionDamage = 50f; // 변수를 선언할거에요 -> 폭발 데미지(50.0)를 explosionDamage에 - [Header("폭발 효과")] - [SerializeField] private GameObject explosionEffectPrefab; // 쾅! 이펙트 프리팹 - [SerializeField] private ParticleSystem fuseEffect; // 몸에서 불꽃 튀는 이펙트 (선택) - [SerializeField] private AudioClip fuseSound; // 치익~ 소리 - [SerializeField] private AudioClip explosionSound; // 쾅! 소리 + [Header("폭발 효과")] // 인스펙터 창에 제목을 표시할거에요 -> 폭발 효과 를 + [SerializeField] private GameObject explosionEffectPrefab; // 변수를 선언할거에요 -> 폭발 이펙트 프리팹을 explosionEffectPrefab에 + [SerializeField] private ParticleSystem fuseEffect; // 변수를 선언할거에요 -> 도화선(준비) 이펙트를 fuseEffect에 + [SerializeField] private AudioClip fuseSound; // 변수를 선언할거에요 -> 도화선 소리를 fuseSound에 + [SerializeField] private AudioClip explosionSound; // 변수를 선언할거에요 -> 폭발 소리를 explosionSound에 - [Header("애니메이션")] - [SerializeField] private string runAnimation = "Monster_Run"; // 달리기 - [SerializeField] private string fuseAnimation = "Monster_Fuse"; // 부들부들(폭발 준비) + [Header("애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 을 + [SerializeField] private string runAnimation = "Monster_Run"; // 변수를 선언할거에요 -> 달리기 애니메이션 이름을 runAnimation에 + [SerializeField] private string fuseAnimation = "Monster_Fuse"; // 변수를 선언할거에요 -> 자폭 준비 애니메이션 이름을 fuseAnimation에 - [Header("AI 설정")] - [SerializeField] private float chaseSpeed = 6f; // 이동 속도 (다른 애들보다 빨라야 무서움) - [SerializeField] private float patrolRadius = 5f; - [SerializeField] private float patrolInterval = 2f; + [Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을 + [SerializeField] private float chaseSpeed = 6f; // 변수를 선언할거에요 -> 추격 속도(6.0)를 chaseSpeed에 + [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 + [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에 - private bool isExploding = false; // 폭발 진행 중? - private bool hasExploded = false; // 이미 터졌나? - private float nextPatrolTime; - private bool isPlayerInZone; + private bool isExploding = false; // 변수를 초기화할거에요 -> 폭발 진행 중 여부를 거짓(false)으로 + private bool hasExploded = false; // 변수를 초기화할거에요 -> 이미 터졌는지 여부를 거짓(false)으로 + private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 + private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 초기화 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - protected override void Init() + protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을 { if (agent != null) { - agent.speed = chaseSpeed; // 속도 설정 - agent.stoppingDistance = 0.5f; // 바짝 붙음 + agent.speed = chaseSpeed; // 값을 설정할거에요 -> 이동 속도를 추격 속도로 + agent.stoppingDistance = 0.5f; // 값을 설정할거에요 -> 정지 거리를 아주 짧게(0.5) } - if (animator != null) animator.applyRootMotion = false; + if (animator != null) animator.applyRootMotion = false; // 설정을 바꿀거에요 -> 애니메이션 이동을 끄기로 - if (fuseEffect != null) fuseEffect.Stop(); + if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를 } - protected override void OnResetStats() + protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를 { - isExploding = false; - hasExploded = false; - if (fuseEffect != null) fuseEffect.Stop(); + isExploding = false; // 상태를 바꿀거에요 -> 폭발 진행 상태를 거짓(false)으로 + hasExploded = false; // 상태를 바꿀거에요 -> 폭발 완료 상태를 거짓(false)으로 + if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트를 끄기를 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // AI 로직 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - protected override void ExecuteAILogic() + protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { - if (isHit || isExploding || hasExploded) return; + if (isHit || isExploding || hasExploded) return; // 조건이 맞으면 중단할거에요 -> 피격, 자폭 중, 이미 폭발 상태라면 - float distance = Vector3.Distance(transform.position, playerTransform.position); + float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 // 플레이어가 감지 범위(15m) 안에 있거나 트리거에 닿았으면 추격 - if (isPlayerInZone || distance <= 15f) + if (isPlayerInZone || distance <= 15f) // 조건이 맞으면 실행할거에요 -> 추격 조건이 만족되면 { - ChasePlayer(distance); + ChasePlayer(distance); // 함수를 실행할거에요 -> 추격 및 자폭 처리를 하는 ChasePlayer를 } - else + else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 { - Patrol(); - UpdateMovementAnimation(); + Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을 + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션을 갱신하는 UpdateMovementAnimation을 } } - void ChasePlayer(float distance) + void ChasePlayer(float distance) // 함수를 선언할거에요 -> 거리별 추격 행동을 정하는 ChasePlayer를 { // 1. 폭발 시작 거리 안으로 들어왔다? -> 점화! - if (distance <= triggerRange) + if (distance <= triggerRange) // 조건이 맞으면 실행할거에요 -> 자폭 거리 이내라면 { - StartCoroutine(ExplodeRoutine()); - return; + StartCoroutine(ExplodeRoutine()); // 코루틴을 실행할거에요 -> 자폭 시퀀스인 ExplodeRoutine을 + return; // 중단할거에요 -> 더 이상 이동하지 않도록 } // 2. 아직 멀었다? -> 전력 질주 - if (agent.isOnNavMesh) + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { - agent.isStopped = false; - agent.SetDestination(playerTransform.position); + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고 + agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 // 달리기 애니메이션 - animator.Play(runAnimation); + animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을 } } - void UpdateMovementAnimation() + void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을 { - if (isExploding || isHit) return; + if (isExploding || isHit) return; // 조건이 맞으면 중단할거에요 -> 폭발 중이거나 맞고 있다면 - if (agent.velocity.magnitude > 0.1f) - animator.Play(runAnimation); - else - animator.Play(Monster_Idle); + if (agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면 + animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을 + else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면 + animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을 } - void Patrol() + void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을 { - if (Time.time < nextPatrolTime) return; + if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면 - Vector3 randomPoint = transform.position + new Vector3( + Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을 Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius) ); - if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) - if (agent.isOnNavMesh) agent.SetDestination(hit.position); + if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 + if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 이동하라고 - nextPatrolTime = Time.time + patrolInterval; + nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 💣 폭발 시퀀스 (점화 -> 대기 -> 쾅!) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - IEnumerator ExplodeRoutine() + IEnumerator ExplodeRoutine() // 코루틴 함수를 선언할거에요 -> 자폭 진행 과정인 ExplodeRoutine을 { - if (hasExploded) yield break; + if (hasExploded) yield break; // 조건이 맞으면 종료할거에요 -> 이미 터졌다면 - isExploding = true; - hasExploded = true; - isAttacking = true; // 부모 클래스 간섭 방지 + isExploding = true; // 상태를 바꿀거에요 -> 폭발 진행 상태를 참(true)으로 + hasExploded = true; // 상태를 바꿀거에요 -> 폭발 완료 상태를 참(true)으로 (중복 방지) + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 (부모 간섭 방지) // ⭐ [핵심] 급브레이크! (문워크 방지) - if (agent.isOnNavMesh) + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { - agent.isStopped = true; - agent.ResetPath(); // 목적지 삭제 - agent.velocity = Vector3.zero; // 속도 0 + agent.isStopped = true; // 명령을 내릴거에요 -> 멈추라고 + agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 지우라고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } - Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)"); + Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)"); // 로그를 출력할거에요 -> 자폭 예고 메시지를 // 1. 부들부들 떨기 (폭발 준비 모션) - if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation); + if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation); // 재생할거에요 -> 준비 애니메이션을 // 2. 치익~ 소리 & 이펙트 - if (fuseEffect != null) fuseEffect.Play(); - if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound); + if (fuseEffect != null) fuseEffect.Play(); // 실행할거에요 -> 도화선 이펙트를 + if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound); // 재생할거에요 -> 도화선 소리를 // 3. 도망갈 시간 주기 - yield return new WaitForSeconds(fuseTime); + yield return new WaitForSeconds(fuseTime); // 기다릴거에요 -> 폭발 지연 시간만큼 // 4. 쾅! - PerformExplosion(); + PerformExplosion(); // 함수를 실행할거에요 -> 실제 폭발 처리를 } - void PerformExplosion() + void PerformExplosion() // 함수를 선언할거에요 -> 폭발 데미지와 이펙트를 처리하는 PerformExplosion을 { - Debug.Log("💥💥💥 쾅!!!"); + Debug.Log("💥💥💥 쾅!!!"); // 로그를 출력할거에요 -> 폭발 메시지를 // 폭발 이펙트 생성 (Particle System) - if (explosionEffectPrefab != null) + if (explosionEffectPrefab != null) // 조건이 맞으면 실행할거에요 -> 폭발 프리팹이 있다면 { - Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); + Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 폭발 이펙트를 현재 위치에 } // 폭발음 - if (explosionSound != null) + if (explosionSound != null) // 조건이 맞으면 실행할거에요 -> 폭발음이 있다면 { - AudioSource.PlayClipAtPoint(explosionSound, transform.position, 1f); + AudioSource.PlayClipAtPoint(explosionSound, transform.position, 1f); // 재생할거에요 -> 폭발 소리를 해당 위치에서 } // 주변 데미지 처리 - DamageNearbyTargets(); + DamageNearbyTargets(); // 함수를 실행할거에요 -> 주변 대상에게 데미지를 주는 DamageNearbyTargets를 // 몬스터 사망 처리 (MonsterClass 기능 사용) - Die(); + Die(); // 함수를 실행할거에요 -> 몬스터를 죽게 만드는 Die를 } - void DamageNearbyTargets() + void DamageNearbyTargets() // 함수를 선언할거에요 -> 폭발 범위 내 데미지를 입히는 DamageNearbyTargets를 { // 폭발 범위(Sphere) 안에 있는 모든 물체 검사 - Collider[] hitColliders = Physics.OverlapSphere(transform.position, explodeRange); + Collider[] hitColliders = Physics.OverlapSphere(transform.position, explodeRange); // 배열에 담을거에요 -> 폭발 범위 내의 충돌체들을 - foreach (Collider hit in hitColliders) + foreach (Collider hit in hitColliders) // 반복할거에요 -> 감지된 모든 충돌체에 대해 { // 플레이어 데미지 - if (hit.CompareTag("Player")) + if (hit.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 대상이 플레이어라면 { - if (hit.TryGetComponent(out var playerHealth)) + if (hit.TryGetComponent(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를 { - if (!playerHealth.isInvincible) + if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면 { - playerHealth.TakeDamage(explosionDamage); - Debug.Log($"💥 플레이어 폭사! 데미지: {explosionDamage}"); + playerHealth.TakeDamage(explosionDamage); // 함수를 실행할거에요 -> 폭발 데미지를 입히는 TakeDamage를 + Debug.Log($"💥 플레이어 폭사! 데미지: {explosionDamage}"); // 로그를 출력할거에요 -> 피격 메시지를 } } } // (선택사항) 주변 몬스터도 팀킬? - else if (hit.CompareTag("Enemy") && hit.gameObject != gameObject) + else if (hit.CompareTag("Enemy") && hit.gameObject != gameObject) // 조건이 맞으면 실행할거에요 -> 대상이 다른 몬스터라면 { // 팀킬 로직이 필요하면 여기에 추가 } @@ -217,24 +217,24 @@ public class ExplodeMonster : MonsterClass // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 자폭병은 때려도 폭발 안 멈춤 (취향에 따라 수정 가능) - protected override void OnStartHit() { } + protected override void OnStartHit() { } // 함수를 비워둘거에요 -> 피격 시 아무 행동도 안 하게 (폭발 캔슬 방지) // 죽을 때 퓨즈 끄기 - protected override void OnDie() + protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 정리를 위해 { - if (fuseEffect != null && fuseEffect.isPlaying) fuseEffect.Stop(); + if (fuseEffect != null && fuseEffect.isPlaying) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를 } - private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } - private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } + private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 진입 시 감지 상태를 참으로 + private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어 이탈 시 감지 상태를 거짓으로 // 에디터에서 범위 보여주기 - private void OnDrawGizmosSelected() + private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 범위를 그리는 OnDrawGizmosSelected를 { - Gizmos.color = Color.red; - Gizmos.DrawWireSphere(transform.position, triggerRange); // 감지 범위 + Gizmos.color = Color.red; // 색상을 설정할거에요 -> 감지 범위 색을 빨간색으로 + Gizmos.DrawWireSphere(transform.position, triggerRange); // 그림을 그릴거에요 -> 자폭 감지 범위를 - Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); - Gizmos.DrawSphere(transform.position, explodeRange); // 폭발 데미지 범위 + Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); // 색상을 설정할거에요 -> 폭발 범위 색을 주황색 반투명으로 + Gizmos.DrawSphere(transform.position, explodeRange); // 그림을 그릴거에요 -> 폭발 데미지 범위를 } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/Monster Weapon.cs b/Assets/Scripts/Enemy/AI/Monster Weapon.cs index bb93aafe..511234dc 100644 --- a/Assets/Scripts/Enemy/AI/Monster Weapon.cs +++ b/Assets/Scripts/Enemy/AI/Monster Weapon.cs @@ -1,77 +1,80 @@ -using System.Collections.Generic; -using UnityEngine; +using System.Collections.Generic; // 제네릭 컬렉션을 쓰기 위해 불러올거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class MonsterWeapon : MonoBehaviour -{ - [Header("무기 설정")] - [Tooltip("이 무기 고유의 공격력 (예: 10)")] - [SerializeField] private float weaponBaseDamage = 10f; +public class MonsterWeapon : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterWeapon을 +{ // 코드 블록을 시작할거에요 -> MonsterWeapon 범위를 - private float _finalDamage; - [SerializeField] private BoxCollider _weaponCollider; + [Header("무기 설정")] // 인스펙터에 제목을 표시할거에요 -> 무기 설정을 + [Tooltip("이 무기 고유의 공격력 (예: 10)")] // 인스펙터에 툴팁을 달거에요 -> weaponBaseDamage 설명을 + [SerializeField] private float weaponBaseDamage = 10f; // 변수를 선언할거에요 -> 무기 기본 공격력을 10으로 - private void Awake() - { - _weaponCollider = GetComponent(); - _finalDamage = weaponBaseDamage; - //DisableHitBox(); - EnableHitBox(); - } + private float _finalDamage; // 변수를 선언할거에요 -> 최종 데미지를 저장할 _finalDamage를 + [SerializeField] private BoxCollider _weaponCollider; // 변수를 선언할거에요 -> 무기 콜라이더를 저장할 _weaponCollider를 - public void SetDamage(float monsterStrength) - { - _finalDamage = weaponBaseDamage + monsterStrength; - Debug.Log($"✅ [무기] 데미지 설정됨: 총 {_finalDamage}"); - } + private void Awake() // 함수를 선언할거에요 -> 시작 시 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + _weaponCollider = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 오브젝트의 BoxCollider를 찾아 _weaponCollider에 + _finalDamage = weaponBaseDamage; // 값을 넣을거에요 -> 최종 데미지를 기본 데미지로 초기화 + //DisableHitBox(); // 주석 처리할거에요 -> 시작 시 판정을 끄는 코드(현재는 안 씀) + EnableHitBox(); // 함수를 실행할거에요 -> 시작하자마자 판정을 켜기(현재 설정) + } // 코드 블록을 끝낼거에요 -> Awake를 - public void EnableHitBox() - { - Debug.Log("enabletest"); + public void SetDamage(float monsterStrength) // 함수를 선언할거에요 -> 몬스터 스탯을 받아 최종 데미지를 세팅하는 SetDamage를 + { // 코드 블록을 시작할거에요 -> SetDamage 범위를 + _finalDamage = weaponBaseDamage + monsterStrength; // 값을 계산할거에요 -> 기본 데미지 + 몬스터 힘으로 최종 데미지 설정 + Debug.Log($"✅ [무기] 데미지 설정됨: 총 {_finalDamage}"); // 로그를 찍을거에요 -> 최종 데미지가 얼마인지 + } // 코드 블록을 끝낼거에요 -> SetDamage를 - if (_weaponCollider != null) - { - Debug.Log("setcollider"); - _weaponCollider.enabled = true; - Debug.Log("🔴 [무기] 공격 판정 ON! (휘두르기 시작)"); // 로그 추가 - } - } + public void EnableHitBox() // 함수를 선언할거에요 -> 공격 판정을 켜는 EnableHitBox를 + { // 코드 블록을 시작할거에요 -> EnableHitBox 범위를 + Debug.Log("enabletest"); // 로그를 찍을거에요 -> 함수가 호출됐는지 확인용 - public void DisableHitBox() - { - if (_weaponCollider != null) - { - _weaponCollider.enabled = false; - Debug.Log("⚪ [무기] 공격 판정 OFF (휘두르기 끝)"); // 로그 추가 - } - } + if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 정상적으로 잡혔는지 + { // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리 + Debug.Log("setcollider"); // 로그를 찍을거에요 -> 콜라이더 enable 직전 확인용 + _weaponCollider.enabled = true; // 값을 바꿀거에요 -> 트리거 판정을 켜기 + Debug.Log("🔴 [무기] 공격 판정 ON! (휘두르기 시작)"); // 로그를 찍을거에요 -> 판정이 켜졌음을 + } // 코드 블록을 끝낼거에요 -> 콜라이더 처리 + } // 코드 블록을 끝낼거에요 -> EnableHitBox를 - // ⭐ 여기가 핵심! 닿는 모든 것을 기록함 - private void OnTriggerEnter(Collider other) - { - // 1. 무엇이든 닿으면 일단 로그를 찍음 (범인 색출) - Debug.Log($"💥 [충돌 감지] 무기가 닿은 것: {other.name} / 태그: {other.tag}"); + public void DisableHitBox() // 함수를 선언할거에요 -> 공격 판정을 끄는 DisableHitBox를 + { // 코드 블록을 시작할거에요 -> DisableHitBox 범위를 + if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 있는지 + { // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리 + _weaponCollider.enabled = false; // 값을 바꿀거에요 -> 트리거 판정을 끄기 + Debug.Log("⚪ [무기] 공격 판정 OFF (휘두르기 끝)"); // 로그를 찍을거에요 -> 판정이 꺼졌음을 + } // 코드 블록을 끝낼거에요 -> 콜라이더 처리 + } // 코드 블록을 끝낼거에요 -> DisableHitBox를 - if (other.CompareTag("Player")) - { - PlayerHealth playerHealth = other.GetComponent(); + // ⭐ 여기가 핵심! 닿는 모든 것을 기록함 // 설명을 적을거에요 -> 트리거 충돌 감지 핵심 구간임을 + private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 다른 콜라이더가 들어오면 호출되는 OnTriggerEnter를 + { // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를 + // 1. 무엇이든 닿으면 일단 로그를 찍음 (범인 색출) // 설명을 적을거에요 -> 뭐랑 부딪히는지 디버깅용 + Debug.Log($"💥 [충돌 감지] 무기가 닿은 것: {other.name} / 태그: {other.tag}"); // 로그를 찍을거에요 -> 충돌한 오브젝트 이름/태그를 - if (playerHealth != null) - { - if (!playerHealth.isInvincible) - { - playerHealth.TakeDamage(_finalDamage); - Debug.Log($"⚔️ [적중] 플레이어 체력 깎임! (-{_finalDamage})"); - } - else - { - Debug.Log("🛡️ [방어] 플레이어가 무적 상태입니다."); - } - DisableHitBox(); - } - else - { - Debug.LogError("⚠️ [오류] 플레이어 태그는 있는데 'PlayerHealth' 스크립트가 없습니다!"); - } - } - } -} \ No newline at end of file + if (other.CompareTag("Player")) // 조건을 검사할거에요 -> 닿은 대상이 Player 태그인지 + { // 코드 블록을 시작할거에요 -> 플레이어일 때 처리 + PlayerHealth playerHealth = other.GetComponent(); // 컴포넌트를 가져올거에요 -> 플레이어의 PlayerHealth를 찾아 playerHealth에 + + if (playerHealth != null) // 조건을 검사할거에요 -> PlayerHealth가 실제로 붙어있는지 + { // 코드 블록을 시작할거에요 -> PlayerHealth가 있을 때 처리 + if (!playerHealth.isInvincible) // 조건을 검사할거에요 -> 플레이어가 무적인지 아닌지 + { // 코드 블록을 시작할거에요 -> 무적이 아닐 때 처리 + playerHealth.TakeDamage(_finalDamage); // 함수를 실행할거에요 -> 플레이어에게 최종 데미지 적용 + Debug.Log($"⚔️ [적중] 플레이어 체력 깎임! (-{_finalDamage})"); // 로그를 찍을거에요 -> 데미지가 들어갔음을 + } // 코드 블록을 끝낼거에요 -> 무적 아닐 때 처리 + else // 조건이 아니면 실행할거에요 -> 무적이면 + { // 코드 블록을 시작할거에요 -> 무적일 때 처리 + Debug.Log("🛡️ [방어] 플레이어가 무적 상태입니다."); // 로그를 찍을거에요 -> 무적이라 데미지 안 들어감을 + } // 코드 블록을 끝낼거에요 -> 무적 처리 + + DisableHitBox(); // 함수를 실행할거에요 -> 한 번 맞추면 바로 판정 끄기(중복 타격 방지) + } // 코드 블록을 끝낼거에요 -> PlayerHealth 있을 때 처리 + else // 조건이 아니면 실행할거에요 -> PlayerHealth가 없으면 + { // 코드 블록을 시작할거에요 -> 오류 처리 + Debug.LogError("⚠️ [오류] 플레이어 태그는 있는데 'PlayerHealth' 스크립트가 없습니다!"); // 에러 로그를 찍을거에요 -> 컴포넌트 누락 경고 + } // 코드 블록을 끝낼거에요 -> 오류 처리 + } // 코드 블록을 끝낼거에요 -> 플레이어 처리 + } // 코드 블록을 끝낼거에요 -> OnTriggerEnter를 + +} // 코드 블록을 끝낼거에요 -> MonsterWeapon을 \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/MonsterClass.cs b/Assets/Scripts/Enemy/AI/MonsterClass.cs index 7ccc6fba..94b99598 100644 --- a/Assets/Scripts/Enemy/AI/MonsterClass.cs +++ b/Assets/Scripts/Enemy/AI/MonsterClass.cs @@ -1,155 +1,163 @@ -using UnityEngine; -using UnityEngine.AI; -using System.Collections; -using System; - - +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using System; // 기본 시스템 기능(Action 등)을 사용할거에요 -> System을 /// /// 몬스터 기본 클래스 (공통 기능만) /// - 공격 방식은 자식 클래스에서 구현 /// -public abstract class MonsterClass : MonoBehaviour, IDamageable +public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 MonsterClass를 { - [Header("--- 최적화 설정 ---")] - protected Renderer mobRenderer; - protected Transform playerTransform; - [SerializeField] protected float optimizationDistance = 40f; + [Header("--- 최적화 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최적화 설정 --- 을 + protected Renderer mobRenderer; // 변수를 선언할거에요 -> 몬스터의 렌더러(외형) 컴포넌트를 담을 mobRenderer를 + protected Transform playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 playerTransform을 + [SerializeField] protected float optimizationDistance = 40f; // 변수를 선언할거에요 -> 몬스터 최적화(AI 정지) 거리(40.0)를 optimizationDistance에 - [Header("몬스터 기본 스탯")] - [SerializeField] protected float maxHP = 100f; - [SerializeField] protected float attackDamage = 10f; - [SerializeField] protected int expReward = 10; - [SerializeField] protected float moveSpeed = 3.5f; + [Header("몬스터 기본 스탯")] // 인스펙터 창에 제목을 표시할거에요 -> 몬스터 기본 스탯 을 + [SerializeField] protected float maxHP = 100f; // 변수를 선언할거에요 -> 최대 체력(100.0)을 maxHP에 + [SerializeField] protected float attackDamage = 10f; // 변수를 선언할거에요 -> 공격력(10.0)을 attackDamage에 + [SerializeField] protected int expReward = 10; // 변수를 선언할거에요 -> 처치 시 경험치 보상(10)을 expReward에 + [SerializeField] protected float moveSpeed = 3.5f; // 변수를 선언할거에요 -> 이동 속도(3.5)를 moveSpeed에 - protected float currentHP; - public event Action OnHealthChanged; + protected float currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 currentHP를 + public event Action OnHealthChanged; // 이벤트를 선언할거에요 -> 체력이 변할 때 알릴 OnHealthChanged를 - [Header("전투 / 무기 (선택사항)")] + [Header("전투 / 무기 (선택사항)")] // 인스펙터 창에 제목을 표시할거에요 -> 전투 / 무기 (선택사항) 을 // ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨) - [SerializeField] protected MonsterWeapon myWeapon; + [SerializeField] protected MonsterWeapon myWeapon; // 변수를 선언할거에요 -> 몬스터가 장착한 무기 스크립트를 myWeapon에 - [Header("피격 / 사망 / 대기 애니메이션")] - [SerializeField] protected string Monster_Idle = "Monster_Idle"; - [SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; - [SerializeField] protected string Monster_Die = "Monster_Die"; + [Header("피격 / 사망 / 대기 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 피격 / 사망 / 대기 애니메이션 을 + [SerializeField] protected string Monster_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름("Monster_Idle")을 Monster_Idle에 + [SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; // 변수를 선언할거에요 -> 피격 애니메이션 이름("Monster_GetDamage")을 Monster_GetDamage에 + [SerializeField] protected string Monster_Die = "Monster_Die"; // 변수를 선언할거에요 -> 사망 애니메이션 이름("Monster_Die")을 Monster_Die에 - protected Animator animator; - protected NavMeshAgent agent; - protected AudioSource audioSource; - protected bool isHit, isDead, isAttacking; + protected Animator animator; // 변수를 선언할거에요 -> 애니메이션 제어 컴포넌트를 담을 animator를 + protected NavMeshAgent agent; // 변수를 선언할거에요 -> 길찾기 에이전트 컴포넌트를 담을 agent를 + protected AudioSource audioSource; // 변수를 선언할거에요 -> 소리 재생 컴포넌트를 담을 audioSource를 - public bool IsAggroed { get; protected set; } + // ⭐ [핵심] 자식 클래스에서 공유할 상태 변수들 (중복 선언 방지) + protected bool isHit; + protected bool isDead; + protected bool isAttacking; - [Header("AI 설정")] - [SerializeField] protected float attackRestDuration = 1.5f; - protected bool isResting; + public bool IsAggroed { get; protected set; } // 프로퍼티를 선언할거에요 -> 어그로(전투) 상태 여부를 외부에서 읽기만 가능하게 IsAggroed에 - public static System.Action OnMonsterKilled; + [Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을 + [SerializeField] protected float attackRestDuration = 1.5f; // 변수를 선언할거에요 -> 공격 후 대기 시간(1.5초)을 attackRestDuration에 + protected bool isResting; // 변수를 선언할거에요 -> 휴식 중인지 여부를 저장할 isResting을 - [Header("공통 사운드/이펙트")] - [SerializeField] protected AudioClip hitSound, deathSound; - [SerializeField] protected GameObject deathEffectPrefab; - [SerializeField] protected ParticleSystem hitEffect; - [SerializeField] protected Transform impactSpawnPoint; + public static System.Action OnMonsterKilled; // 정적 이벤트를 선언할거에요 -> 몬스터 처치 시 경험치를 전달할 OnMonsterKilled를 + + [Header("공통 사운드/이펙트")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 사운드/이펙트 를 + [SerializeField] protected AudioClip hitSound, deathSound; // 변수를 선언할거에요 -> 피격음과 사망음 오디오 클립을 + [SerializeField] protected GameObject deathEffectPrefab; // 변수를 선언할거에요 -> 사망 시 재생할 이펙트 프리팹을 + [SerializeField] protected ParticleSystem hitEffect; // 변수를 선언할거에요 -> 피격 시 재생할 파티클 시스템을 + [SerializeField] protected Transform impactSpawnPoint; // 변수를 선언할거에요 -> 이펙트가 생성될 위치를 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 생명주기 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - protected virtual void Awake() + protected virtual void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 호출되는 Awake를 { - mobRenderer = GetComponentInChildren(); - animator = GetComponent(); - agent = GetComponent(); - audioSource = GetComponent(); - if (agent != null) agent.speed = moveSpeed; + mobRenderer = GetComponentInChildren(); // 컴포넌트를 가져올거에요 -> 자식 오브젝트의 렌더러를 mobRenderer에 + animator = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 애니메이터를 animator에 + agent = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 길찾기 에이전트를 agent에 + audioSource = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 오디오 소스를 audioSource에 + if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 설정할거에요 -> 에이전트가 있다면 이동 속도를 moveSpeed로 } - protected virtual void OnEnable() + protected virtual void OnEnable() // 함수를 실행할거에요 -> 오브젝트가 활성화될 때 호출되는 OnEnable을 { - playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; - if (mobRenderer != null) mobRenderer.enabled = true; - Init(); - if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); + playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // 오브젝트를 찾을거에요 -> "Player" 태그를 가진 오브젝트의 위치를 playerTransform에 + if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 보이게 켜기(true)를 + Init(); // 함수를 실행할거에요 -> 자식 클래스에서 정의할 초기화 함수 Init을 + if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에 등록하기를 } - protected virtual void OnDisable() + protected virtual void OnDisable() // 함수를 실행할거에요 -> 오브젝트가 비활성화될 때 호출되는 OnDisable을 { - if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); + if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에서 제외하기를 } - public void ResetStats() + // ⭐ [수정] Update를 virtual로 선언하여 자식이 재정의 가능하게 함 + protected virtual void Update() { - isDead = false; - IsAggroed = false; - currentHP = maxHP; - OnHealthChanged?.Invoke(currentHP, maxHP); + OnManagedUpdate(); // 기본적으로 매니저 업데이트 로직을 수행 (일반 몬스터용) + } - Collider col = GetComponent(); - if (col != null) col.enabled = true; + public void ResetStats() // 함수를 선언할거에요 -> 몬스터 상태를 초기화하는 ResetStats를 + { + isDead = false; // 상태를 바꿀거에요 -> 사망 상태를 거짓(false)으로 + IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로 + currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로 + OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 변경되었음을 UI 등에 - if (agent != null) agent.speed = moveSpeed; // 스피드 초기화 + Collider col = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에 + if (col != null) col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 다시 켜기(true)를 - OnResetStats(); + if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 속도를 초기화하기를 + + OnResetStats(); // 함수를 실행할거에요 -> 자식 클래스의 추가 초기화 함수 OnResetStats를 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 추상 메서드 (자식이 반드시 구현) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - protected virtual void Init() { } - protected abstract void ExecuteAILogic(); - protected virtual void OnResetStats() { } + protected virtual void Init() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 초기화 함수 Init을 + protected abstract void ExecuteAILogic(); // 추상 함수를 선언할거에요 -> 자식에서 반드시 구현해야 할 AI 로직 ExecuteAILogic을 + protected virtual void OnResetStats() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 스탯 초기화 함수 OnResetStats를 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 공통 기능 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - public virtual void Reactivate() + public virtual void Reactivate() // 함수를 선언할거에요 -> 몬스터를 재활성화하는 Reactivate를 { - if (agent != null && agent.isOnNavMesh) + if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 있고 바닥에 있다면 { - agent.isStopped = false; - if (IsAggroed && playerTransform != null) + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고 + if (IsAggroed && playerTransform != null) // 조건이 맞으면 실행할거에요 -> 어그로가 끌렸고 플레이어가 있다면 { - agent.SetDestination(playerTransform.position); + agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 } } - if (mobRenderer != null) mobRenderer.enabled = true; + if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 다시 켜기(true)를 } - public void OnManagedUpdate() + public virtual void OnManagedUpdate() // 함수를 선언할거에요 -> 매니저가 호출해줄 업데이트 함수 OnManagedUpdate를 { - if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; + if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; // 조건이 맞으면 중단할거에요 -> 죽었거나, 플레이어가 없거나, 꺼져 있다면 - float distance = Vector3.Distance(transform.position, playerTransform.position); + float distance = Vector3.Distance(transform.position, playerTransform.position); // 값을 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에 - if (distance > optimizationDistance && !IsAggroed) + if (distance > optimizationDistance && !IsAggroed) // 조건이 맞으면 실행할거에요 -> 거리가 멀고 어그로 상태가 아니라면 { - StopMovement(); - if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; - return; + StopMovement(); // 함수를 실행할거에요 -> 움직임을 멈추는 StopMovement를 + if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; // 조건이 맞으면 실행할거에요 -> 렌더러가 켜져 있다면 끄기(false)를 (최적화) + return; // 중단할거에요 -> AI 로직을 실행하지 않도록 함수를 } - if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; - if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } - if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; + if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 꺼져 있다면 다시 켜기(true)를 + if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } // 조건이 맞으면 실행할거에요 -> 렌더러가 화면에 보이지 않는다면 멈추고 중단하기를 + if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 멈춰있다면 다시 움직이게(false) 하기를 - ExecuteAILogic(); + ExecuteAILogic(); // 함수를 실행할거에요 -> 실제 몬스터의 AI 로직을 } - protected void StopMovement() + protected void StopMovement() // 함수를 선언할거에요 -> 이동을 멈추는 StopMovement를 { - if (agent != null && agent.isOnNavMesh) + if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상 작동 중이라면 { - agent.isStopped = true; - agent.velocity = Vector3.zero; + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 0으로 } - if (animator != null) + if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 { - animator.SetFloat("Speed", 0f); - animator.Play(Monster_Idle); + animator.SetFloat("Speed", 0f); // 값을 전달할거에요 -> 속도 파라미터를 0으로 + animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션(Monster_Idle)을 } } @@ -157,210 +165,195 @@ public abstract class MonsterClass : MonoBehaviour, IDamageable // 피격 / 사망 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - public virtual void TakeDamage(float amount) + public virtual void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 줄 때 호출할 TakeDamage를 { - OnDamaged(amount); + OnDamaged(amount); // 함수를 실행할거에요 -> 실제 피격 처리를 담당하는 OnDamaged를 } - public virtual void OnDamaged(float damage) + public virtual void OnDamaged(float damage) // 함수를 선언할거에요 -> 데미지 계산 로직인 OnDamaged를 { - if (isDead) return; - IsAggroed = true; - currentHP -= damage; - OnHealthChanged?.Invoke(currentHP, maxHP); - if (currentHP <= 0) { Die(); return; } - if (!isHit) StartHit(); + if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면 + IsAggroed = true; // 상태를 바꿀거에요 -> 공격받았으니 어그로 상태를 참(true)으로 + currentHP -= damage; // 값을 뺄거에요 -> 체력에서 데미지(damage)만큼을 + OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 깎였음을 + if (currentHP <= 0) { Die(); return; } // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 함수(Die)를 + if (!isHit) StartHit(); // 조건이 맞으면 실행할거에요 -> 피격 상태가 아니라면 피격 연출(StartHit)을 } - protected virtual void StartHit() + protected virtual void StartHit() // 함수를 선언할거에요 -> 피격 연출을 시작하는 StartHit을 { - isHit = true; - isAttacking = false; - isResting = false; - StopAllCoroutines(); + isHit = true; // 상태를 바꿀거에요 -> 피격 중 상태를 참(true)으로 + isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로 + isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 + StopAllCoroutines(); // 중단할거에요 -> 실행 중인 모든 코루틴을 - OnStartHit(); + OnStartHit(); // 함수를 실행할거에요 -> 자식 클래스의 추가 피격 처리 OnStartHit을 - if (agent && agent.isOnNavMesh) + if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면 { - agent.isStopped = true; - agent.velocity = Vector3.zero; + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } - animator.Play(Monster_GetDamage, 0, 0f); - if (hitEffect) hitEffect.Play(); - if (hitSound) audioSource.PlayOneShot(hitSound); + animator.Play(Monster_GetDamage, 0, 0f); // 재생할거에요 -> 피격 애니메이션(Monster_GetDamage)을 + if (hitEffect) hitEffect.Play(); // 조건이 맞으면 실행할거에요 -> 피격 이펙트가 있다면 재생하기를 + if (hitSound) audioSource.PlayOneShot(hitSound); // 조건이 맞으면 실행할거에요 -> 피격 사운드가 있다면 재생하기를 } - protected virtual void OnStartHit() { } + protected virtual void OnStartHit() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 피격 시작 함수 OnStartHit을 - public virtual void OnHitEnd() + public virtual void OnHitEnd() // 함수를 선언할거에요 -> 피격 연출이 끝났을 때 호출할 OnHitEnd를 { - isHit = false; - if (agent && agent.isOnNavMesh) agent.isStopped = false; + isHit = false; // 상태를 바꿀거에요 -> 피격 중 상태를 거짓(false)으로 + if (agent && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 다시 움직이게(false) 하기를 } - protected virtual void Die() + protected virtual void Die() // 함수를 선언할거에요 -> 몬스터 사망을 처리하는 Die를 { - if (isDead) return; - isDead = true; - IsAggroed = false; + if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽은 상태라면 + isDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참(true)으로 + IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로 - OnDie(); + OnDie(); // 함수를 실행할거에요 -> 자식 클래스의 추가 사망 처리 OnDie를 - OnMonsterKilled?.Invoke(expReward); - Collider col = GetComponent(); - if (col != null) col.enabled = false; + OnMonsterKilled?.Invoke(expReward); // 이벤트를 알릴거에요 -> 몬스터 처치와 경험치 보상(expReward)을 + Collider col = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에 + if (col != null) col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 끄기(false)를 (시체 밟기 방지) - if (agent && agent.isOnNavMesh) + if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면 { - agent.isStopped = true; - agent.velocity = Vector3.zero; + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } - animator.Play(Monster_Die, 0, 0f); - if (deathSound) audioSource.PlayOneShot(deathSound); - Invoke("ReturnToPool", 1.5f); + animator.Play(Monster_Die, 0, 0f); // 재생할거에요 -> 사망 애니메이션(Monster_Die)을 + if (deathSound) audioSource.PlayOneShot(deathSound); // 조건이 맞으면 실행할거에요 -> 사망 사운드가 있다면 재생하기를 + Invoke("ReturnToPool", 1.5f); // 예약을 걸거에요 -> 1.5초 뒤에 풀로 반환하는 ReturnToPool 함수를 } - protected virtual void OnDie() { } + protected virtual void OnDie() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 사망 처리 함수 OnDie를 - public bool IsDead => isDead; - protected void ReturnToPool() => gameObject.SetActive(false); + public bool IsDead => isDead; // 프로퍼티를 선언할거에요 -> 외부에서 사망 여부를 확인할 수 있는 IsDead를 + protected void ReturnToPool() => gameObject.SetActive(false); // 함수를 선언할거에요 -> 오브젝트를 비활성화해서 풀로 돌려보내는 ReturnToPool을 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ⭐ 공격 이벤트 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - public virtual void OnAttackStart() + // ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언 + public virtual void OnAttackStart() // 함수를 선언할거에요 -> 공격 시작 시 호출되는 OnAttackStart를 { - isAttacking = true; - isResting = false; + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 + isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 - if (myWeapon != null) + if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면 { - myWeapon.EnableHitBox(); + myWeapon.EnableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 켜는 EnableHitBox를 } } - public virtual void OnAttackEnd() + // ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언 + public virtual void OnAttackEnd() // 함수를 선언할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를 { - if (myWeapon != null) + if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면 { - myWeapon.DisableHitBox(); + myWeapon.DisableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 끄는 DisableHitBox를 } - isAttacking = false; - if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); + isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로 + if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴(RestAfterAttack)을 } - protected virtual IEnumerator RestAfterAttack() + protected virtual IEnumerator RestAfterAttack() // 코루틴 함수를 선언할거에요 -> 공격 후 잠시 쉬는 RestAfterAttack을 { - isResting = true; - yield return new WaitForSeconds(attackRestDuration); - isResting = false; + isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로 + yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간(attackRestDuration)만큼 + isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // ⭐ 상태 이상 시스템 (추가된 부분) + // ⭐ 상태 이상 시스템 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - /// - /// 상태이상 적용 (PlayerArrow에서 호출) - /// - public void ApplyStatusEffect(StatusEffectType type, float damage, float duration) + public void ApplyStatusEffect(StatusEffectType type, float damage, float duration) // 함수를 선언할거에요 -> 상태이상을 적용하는 ApplyStatusEffect를 { - if (isDead) return; + if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면 - switch (type) + switch (type) // 분기할거에요 -> 상태이상 종류(type)에 따라 { - case StatusEffectType.Burn: - StartCoroutine(BurnCoroutine(damage, duration)); + case StatusEffectType.Burn: // 조건이 맞으면 실행할거에요 -> 화상(Burn)이라면 + StartCoroutine(BurnCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 화상 효과를 주는 BurnCoroutine을 break; - case StatusEffectType.Slow: - StartCoroutine(SlowCoroutine(damage, duration)); + case StatusEffectType.Slow: // 조건이 맞으면 실행할거에요 -> 슬로우(Slow)라면 + StartCoroutine(SlowCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 느리게 만드는 SlowCoroutine을 break; - case StatusEffectType.Poison: - StartCoroutine(PoisonCoroutine(damage, duration)); + case StatusEffectType.Poison: // 조건이 맞으면 실행할거에요 -> 독(Poison)이라면 + StartCoroutine(PoisonCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 독 효과를 주는 PoisonCoroutine을 break; - case StatusEffectType.Shock: - TakeDamage(damage); // 충격 데미지 즉시 적용 - StartCoroutine(StunCoroutine(0.5f)); // 0.5초 스턴 + case StatusEffectType.Shock: // 조건이 맞으면 실행할거에요 -> 충격(Shock)이라면 + TakeDamage(damage); // 함수를 실행할거에요 -> 충격 데미지를 즉시 적용하기를 + StartCoroutine(StunCoroutine(0.5f)); // 코루틴을 실행할거에요 -> 0.5초간 기절시키는 StunCoroutine을 break; } } - /// - /// 화상: 일정 시간 동안 0.5초마다 틱 데미지 - /// - private IEnumerator BurnCoroutine(float tickDamage, float duration) + private IEnumerator BurnCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 화상 효과를 처리할 BurnCoroutine을 { - float elapsed = 0f; - float tickInterval = 0.5f; - while (elapsed < duration) + float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로 + float tickInterval = 0.5f; // 변수를 초기화할거에요 -> 데미지 간격을 0.5초로 + while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안 { - TakeDamage(tickDamage * tickInterval); - yield return new WaitForSeconds(tickInterval); - elapsed += tickInterval; + TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를 + yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 0.5초의 시간을 + elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 0.5초를 } } - /// - /// 슬로우: 이동속도를 일시적으로 감소 - /// - private IEnumerator SlowCoroutine(float amount, float duration) + private IEnumerator SlowCoroutine(float amount, float duration) // 코루틴 함수를 선언할거에요 -> 슬로우 효과를 처리할 SlowCoroutine을 { - // NavMeshAgent가 있는 경우 속도 조절 - if (agent != null) + if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 { - float originalSpeed = agent.speed; - agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); - yield return new WaitForSeconds(duration); - agent.speed = originalSpeed; // 원래 속도로 복구 + float originalSpeed = agent.speed; // 값을 저장할거에요 -> 원래 속도를 originalSpeed에 + agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); // 값을 변경할거에요 -> 현재 속도를 비율(amount)만큼 줄여서 + yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼 + agent.speed = originalSpeed; // 값을 복구할거에요 -> 속도를 원래대로 } - else + else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면 { - // NavMeshAgent가 없으면 그냥 대기 - yield return new WaitForSeconds(duration); + yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를 } } - /// - /// 독: 지속 데미지 (1초마다) - /// - private IEnumerator PoisonCoroutine(float tickDamage, float duration) + private IEnumerator PoisonCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 독 효과를 처리할 PoisonCoroutine을 { - float elapsed = 0f; - float tickInterval = 1f; - while (elapsed < duration) + float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로 + float tickInterval = 1f; // 변수를 초기화할거에요 -> 데미지 간격을 1초로 + while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안 { - TakeDamage(tickDamage * tickInterval); - yield return new WaitForSeconds(tickInterval); - elapsed += tickInterval; + TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를 + yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 1초의 시간을 + elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 1초를 } } - /// - /// 스턴: 짧은 시간 동안 행동 정지 - /// - private IEnumerator StunCoroutine(float duration) + private IEnumerator StunCoroutine(float duration) // 코루틴 함수를 선언할거에요 -> 기절 효과를 처리할 StunCoroutine을 { - if (agent != null) + if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 { - agent.isStopped = true; - if (animator != null) animator.speed = 0; // 애니메이션도 멈춤 (선택사항) + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고 + if (animator != null) animator.speed = 0; // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 재생 속도를 0으로 (얼음 땡) - yield return new WaitForSeconds(duration); + yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼 - if (!isDead) + if (!isDead) // 조건이 맞으면 실행할거에요 -> 아직 살아있다면 { - agent.isStopped = false; - if (animator != null) animator.speed = 1; + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고 + if (animator != null) animator.speed = 1; // 조건이 맞으면 실행할거에요 -> 애니메이션 속도를 정상(1)으로 } } - else + else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면 { - yield return new WaitForSeconds(duration); + yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를 } } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/NormalMonster.cs b/Assets/Scripts/Enemy/AI/NormalMonster.cs index 78af6565..1ff83c12 100644 --- a/Assets/Scripts/Enemy/AI/NormalMonster.cs +++ b/Assets/Scripts/Enemy/AI/NormalMonster.cs @@ -1,185 +1,182 @@ -using UnityEngine; -using UnityEngine.AI; -using System.Collections; -using System.Collections.Generic; // ✅ 리스트 사용을 위해 필수 추가 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 -public class MeleeMonster : MonsterClass +public class MeleeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 MeleeMonster를 { - [Header("=== 근접 공격 설정 ===")] - //[SerializeField] private MonsterWeapon myWeapon; // ✅ 주석 해제됨 (이제 무기 연결 가능!) - [SerializeField] private float attackRange = 2f; - [SerializeField] private float attackDelay = 1.5f; + [Header("=== 근접 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 근접 공격 설정 === 을 + [SerializeField] private float attackRange = 2f; // 변수를 선언할거에요 -> 공격 사거리(2.0)를 attackRange에 + [SerializeField] private float attackDelay = 1.5f; // 변수를 선언할거에요 -> 공격 딜레이(1.5초)를 attackDelay에 - // 🎁 [수정] 단일 아이템 -> 리스트로 변경 - [Header("=== 드랍 아이템 설정 ===")] - [Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")] - [SerializeField] private List dropItemPrefabs; + [Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을 + [Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private List dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍할 아이템 프리팹 목록을 dropItemPrefabs에 - [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] - [Range(0, 100)][SerializeField] private float dropChance = 30f; // 기본 30% + [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에 - private float lastAttackTime; + private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에 - [Header("공격 / 이동 애니메이션")] - [SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; - [SerializeField] private string Monster_Walk = "Monster_Walk"; + [Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을 + [SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; // 배열을 선언할거에요 -> 공격 애니메이션 이름들을 attackAnimations에 + [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에 - [Header("AI 상세 설정")] - [SerializeField] private float stopBuffer = 0.3f; - [SerializeField] private float patrolRadius = 5f; - [SerializeField] private float patrolInterval = 2f; + [Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을 + [SerializeField] private float stopBuffer = 0.3f; // 변수를 선언할거에요 -> 정지 여유 거리(0.3)를 stopBuffer에 + [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 + [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 대기 시간(2.0)을 patrolInterval에 - private float nextPatrolTime; - private float repathInterval = 0.3f; - private float nextRepathTime; - private int attackIndex; - private bool isPlayerInZone; + private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 + private float repathInterval = 0.3f; // 변수를 선언할거에요 -> 경로 갱신 주기(0.3초)를 repathInterval에 + private float nextRepathTime; // 변수를 선언할거에요 -> 다음 경로 갱신 시간을 nextRepathTime에 + private int attackIndex; // 변수를 선언할거에요 -> 현재 공격 애니메이션 인덱스를 attackIndex에 + private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어가 감지 구역에 있는지 여부를 isPlayerInZone에 - protected override void Init() + protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을 { - if (agent != null) agent.stoppingDistance = attackRange - 0.4f; - if (animator != null) animator.applyRootMotion = false; + if (agent != null) agent.stoppingDistance = attackRange - 0.4f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 사거리보다 약간 짧게 + if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동(Root Motion)을 끄기로 } - protected override void OnResetStats() + protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를 { - if (myWeapon != null) + if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면 { - myWeapon.SetDamage(attackDamage); + myWeapon.SetDamage(attackDamage); // 함수를 실행할거에요 -> 무기 공격력을 몬스터 공격력으로 설정을 } } - protected override void ExecuteAILogic() + protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { - if (isHit || isAttacking || isResting) return; + if (isHit || isAttacking || isResting) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식 중이라면 - float distance = Vector3.Distance(transform.position, playerTransform.position); + float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에 - if (isPlayerInZone || distance <= attackRange * 2f) - HandlePlayerTarget(); - else - Patrol(); + if (isPlayerInZone || distance <= attackRange * 2f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면 + HandlePlayerTarget(); // 함수를 실행할거에요 -> 추격 및 공격 처리를 하는 HandlePlayerTarget을 + else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 + Patrol(); // 함수를 실행할거에요 -> 주변을 배회하는 Patrol을 - UpdateMovementAnimation(); + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 상태를 갱신하는 UpdateMovementAnimation을 } - void HandlePlayerTarget() + void HandlePlayerTarget() // 함수를 선언할거에요 -> 타겟 처리 로직인 HandlePlayerTarget을 { - float distance = Vector3.Distance(transform.position, playerTransform.position); + float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 - if (distance <= attackRange - stopBuffer) - TryAttack(); - else if (Time.time >= nextRepathTime) + if (distance <= attackRange - stopBuffer) // 조건이 맞으면 실행할거에요 -> 사거리 안쪽으로 충분히 들어왔다면 + TryAttack(); // 함수를 실행할거에요 -> 공격을 시도하는 TryAttack을 + else if (Time.time >= nextRepathTime) // 조건이 맞으면 실행할거에요 -> 경로 갱신 시간이 되었다면 { - if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position); - nextRepathTime = Time.time + repathInterval; + if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 + nextRepathTime = Time.time + repathInterval; // 값을 갱신할거에요 -> 다음 경로 갱신 시간을 } } - void TryAttack() + void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을 { - if (Time.time < lastAttackTime + attackDelay) return; - lastAttackTime = Time.time; + if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 아직 쿨타임이 안 지났다면 + lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로 - string attackName = attackAnimations[attackIndex]; - attackIndex = (attackIndex + 1) % attackAnimations.Length; + string attackName = attackAnimations[attackIndex]; // 값을 가져올거에요 -> 현재 인덱스의 공격 애니메이션 이름을 + attackIndex = (attackIndex + 1) % attackAnimations.Length; // 값을 갱신할거에요 -> 다음 공격 인덱스로 (순환) - isAttacking = true; + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 - if (agent.isOnNavMesh) + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 바닥에 있다면 { - agent.isStopped = true; - agent.velocity = Vector3.zero; + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } - animator.Play(attackName, 0, 0f); + animator.Play(attackName, 0, 0f); // 재생할거에요 -> 공격 애니메이션을 처음부터 } - void UpdateMovementAnimation() + void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션을 제어하는 UpdateMovementAnimation을 { - if (isAttacking || isHit || isResting) return; + if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 - if (agent.velocity.magnitude < 0.1f) - animator.Play(Monster_Idle); - else - animator.Play(Monster_Walk); + if (agent.velocity.magnitude < 0.1f) // 조건이 맞으면 실행할거에요 -> 거의 멈춰 있다면 + animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을 + else // 조건이 틀리면 실행할거에요 -> 움직이고 있다면 + animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을 } - void Patrol() + void Patrol() // 함수를 선언할거에요 -> 순찰 로직인 Patrol을 { - if (Time.time < nextPatrolTime) return; + if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 아직 대기 시간이라면 - Vector3 randomPoint = transform.position + new Vector3( + Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 현재 위치 주변의 랜덤한 지점을 Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius) ); - if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) - if (agent.isOnNavMesh) agent.SetDestination(hit.position); + if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 랜덤 지점이 NavMesh 위라면 + if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그 지점으로 이동하라고 - nextPatrolTime = Time.time + patrolInterval; + nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을 } - public override void OnAttackStart() + public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 시 호출되는 OnAttackStart를 { - isAttacking = true; - isResting = false; - if (myWeapon != null) myWeapon.EnableHitBox(); + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 + isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 + if (myWeapon != null) myWeapon.EnableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 켜기를 } - public override void OnAttackEnd() + public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를 { - if (myWeapon != null) myWeapon.DisableHitBox(); - isAttacking = false; - if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); + if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 끄기를 + isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로 + if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴을 } - protected override IEnumerator RestAfterAttack() + protected override IEnumerator RestAfterAttack() // 코루틴 함수를 덮어씌워 실행할거에요 -> 공격 후 휴식하는 RestAfterAttack을 { - isResting = true; - yield return new WaitForSeconds(attackRestDuration); - isResting = false; + isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로 + yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간만큼 + isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 } - protected override void OnStartHit() + protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 피격 시작 시 호출되는 OnStartHit을 { - if (myWeapon != null) myWeapon.DisableHitBox(); + if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 피격 당했으니 공격 판정을 끄기를 } // 🎲 [핵심 수정] 리스트에서 랜덤 뽑기 + 확률 체크 - protected override void OnDie() + protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 호출되는 OnDie를 { - if (myWeapon != null) myWeapon.DisableHitBox(); + if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 공격 판정을 끄기를 // 1. 리스트에 아이템이 하나라도 있는지 확인 - if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) + if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 드랍 테이블이 비어있지 않다면 { // 2. 확률 체크 (0 ~ 100) - float randomValue = Random.Range(0f, 100f); + float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0부터 100 사이의 랜덤값을 - if (randomValue <= dropChance) // 당첨! + if (randomValue <= dropChance) // 당첨! // 조건이 맞으면 실행할거에요 -> 랜덤값이 드랍 확률 이하라면 { // 3. 리스트에서 랜덤하게 하나 뽑기 (0번 ~ 마지막 번호 중 하나) - int randomIndex = Random.Range(0, dropItemPrefabs.Count); - GameObject selectedItem = dropItemPrefabs[randomIndex]; + int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 아이템 리스트 인덱스를 랜덤으로 + GameObject selectedItem = dropItemPrefabs[randomIndex]; // 오브젝트를 가져올거에요 -> 선택된 아이템 프리팹을 - if (selectedItem != null) + if (selectedItem != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면 { - Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity); - // Debug.Log($"🎉 아이템 드랍! ({selectedItem.name})"); + Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에 } } } } - private void OnTriggerEnter(Collider other) + private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거에 들어왔을 때 OnTriggerEnter를 { - if (other.CompareTag("Player")) isPlayerInZone = true; + if (other.CompareTag("Player")) isPlayerInZone = true; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 참(true)으로 } - private void OnTriggerExit(Collider other) + private void OnTriggerExit(Collider other) // 함수를 실행할거에요 -> 트리거에서 나갔을 때 OnTriggerExit을 { - if (other.CompareTag("Player")) isPlayerInZone = false; + if (other.CompareTag("Player")) isPlayerInZone = false; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 거짓(false)으로 } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/Range Monster.cs b/Assets/Scripts/Enemy/AI/Range Monster.cs index 712e30bd..66a3d0d3 100644 --- a/Assets/Scripts/Enemy/AI/Range Monster.cs +++ b/Assets/Scripts/Enemy/AI/Range Monster.cs @@ -1,213 +1,208 @@ -using UnityEngine; -using UnityEngine.AI; -using System.Collections; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 /// /// 통합 원거리 몬스터 (키 작은 플레이어 머리 조준 기능 추가) /// 파일 이름을 반드시 [ UniversalRangedMonster.cs ] 로 맞춰주세요! /// -public class UniversalRangedMonster : MonsterClass +public class UniversalRangedMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 UniversalRangedMonster를 { - public enum AttackStyle { Straight, Lob } + public enum AttackStyle { Straight, Lob } // 열거형을 정의할거에요 -> 공격 방식(직선, 곡사)을 정의하는 AttackStyle을 - [Header("=== 🏹 공격 스타일 선택 ===")] - [SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; + [Header("=== 🏹 공격 스타일 선택 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 🏹 공격 스타일 선택 === 을 + [SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; // 변수를 선언할거에요 -> 공격 스타일(기본 직선)을 attackStyle에 - [Header("공통 설정")] - [SerializeField] private GameObject projectilePrefab; - [SerializeField] private Transform firePoint; - [SerializeField] private float attackRange = 10f; - [SerializeField] private float attackDelay = 2f; - [SerializeField] private float detectRange = 15f; + [Header("공통 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 설정 을 + [SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 발사체 프리팹을 projectilePrefab에 + [SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 발사 위치를 firePoint에 + [SerializeField] private float attackRange = 10f; // 변수를 선언할거에요 -> 공격 사거리(10.0)를 attackRange에 + [SerializeField] private float attackDelay = 2f; // 변수를 선언할거에요 -> 공격 딜레이(2.0초)를 attackDelay에 + [SerializeField] private float detectRange = 15f; // 변수를 선언할거에요 -> 인식 거리(15.0)를 detectRange에 - [Header("🔹 직선 발사 설정 (활/총)")] - [SerializeField] private float projectileSpeed = 20f; - [SerializeField] private float minDistance = 5f; + [Header("🔹 직선 발사 설정 (활/총)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔹 직선 발사 설정 (활/총) 을 + [SerializeField] private float projectileSpeed = 20f; // 변수를 선언할거에요 -> 투사체 속도(20.0)를 projectileSpeed에 + [SerializeField] private float minDistance = 5f; // 변수를 선언할거에요 -> 최소 거리(도망가는 거리)를 minDistance에 - [Header("🔸 곡사 투척 설정 (돌/폭탄)")] - [Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")] - [SerializeField] private bool usePreciseLob = true; + [Header("🔸 곡사 투척 설정 (돌/폭탄)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔸 곡사 투척 설정 (돌/폭탄) 을 + [Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private bool usePreciseLob = true; // 변수를 선언할거에요 -> 정밀 곡사 사용 여부를 usePreciseLob에 - [Tooltip("던지는 각도 (45도가 최대 사거리)")] - [Range(15f, 75f)][SerializeField] private float launchAngle = 45f; + [Tooltip("던지는 각도 (45도가 최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(15f, 75f)][SerializeField] private float launchAngle = 45f; // 변수를 선언할거에요 -> 발사 각도(45도)를 launchAngle에 // ⭐ [추가됨] 조준 높이 보정 (키 작은 플레이어 해결용) - [Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")] - [SerializeField] private float aimHeight = 1.2f; + [Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float aimHeight = 1.2f; // 변수를 선언할거에요 -> 조준 높이 오프셋(1.2)을 aimHeight에 - [Header("🏃‍♂️ 도망 설정")] - [SerializeField] private float fleeDistance = 5f; + [Header("🏃‍♂️ 도망 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 🏃‍♂️ 도망 설정 을 + [SerializeField] private float fleeDistance = 5f; // 변수를 선언할거에요 -> 도망가는 거리(5.0)를 fleeDistance에 - [Header("애니메이션 & 기타")] - [SerializeField] private float throwForce = 15f; // (정밀 모드 꺼졌을 때 사용) - [SerializeField] private float throwUpward = 5f; // (정밀 모드 꺼졌을 때 사용) - [SerializeField] private float reloadTime = 2.0f; - [SerializeField] private GameObject handModel; - [SerializeField] private bool aimAtPlayer = true; + [Header("애니메이션 & 기타")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 & 기타 를 + [SerializeField] private float throwForce = 15f; // 변수를 선언할거에요 -> 기본 투척 힘을 throwForce에 + [SerializeField] private float throwUpward = 5f; // 변수를 선언할거에요 -> 기본 상향 힘을 throwUpward에 + [SerializeField] private float reloadTime = 2.0f; // 변수를 선언할거에요 -> 장전 시간(2.0초)을 reloadTime에 + [SerializeField] private GameObject handModel; // 변수를 선언할거에요 -> 손에 든 무기 모델을 handModel에 + [SerializeField] private bool aimAtPlayer = true; // 변수를 선언할거에요 -> 플레이어 조준 여부를 aimAtPlayer에 - [SerializeField] private string attackAnim = "Monster_Attack"; - [SerializeField] private string walkAnim = "Monster_Walk"; - [SerializeField] private float patrolRadius = 5f; - [SerializeField] private float patrolInterval = 3f; + [SerializeField] private string attackAnim = "Monster_Attack"; // 변수를 선언할거에요 -> 공격 애니메이션 이름을 attackAnim에 + [SerializeField] private string walkAnim = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 walkAnim에 + [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 + [SerializeField] private float patrolInterval = 3f; // 변수를 선언할거에요 -> 순찰 간격(3.0초)을 patrolInterval에 - private float lastAttackTime; - private float nextPatrolTime; - // private bool isPlayerInZone; - private bool isReloading = false; + private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에 + private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 + private bool isReloading = false; // 변수를 선언할거에요 -> 장전 중 여부를 isReloading에 - protected override void Init() + protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을 { - if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; } - if (animator != null) animator.applyRootMotion = false; + if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; } // 조건이 맞으면 설정할거에요 -> 정지 거리를 사거리의 80%로, 속도를 3.5로 + if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로 } - protected override void ExecuteAILogic() + protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { - if (isHit || isAttacking || isResting || isReloading) return; - if (isAttacking && animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) OnAttackEnd(); + if (isHit || isAttacking || isResting || isReloading) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식, 장전 중이라면 + if (isAttacking && animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) OnAttackEnd(); // 조건이 맞으면 실행할거에요 -> 공격 중인데 모션이 대기라면 강제로 공격 종료를 - float dist = Vector3.Distance(transform.position, playerTransform.position); + float dist = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 - if (dist <= detectRange) HandleCombat(dist); - else + if (dist <= detectRange) HandleCombat(dist); // 조건이 맞으면 실행할거에요 -> 감지 거리 이내라면 전투 처리(HandleCombat)를 + else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 { - if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; } - Patrol(); - UpdateMovementAnimation(); + if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; } // 조건이 맞으면 실행할거에요 -> 추격 중이었다면 경로를 취소하고 순찰을 준비하기를 + Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을 + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 갱신을 } } - void HandleCombat(float dist) + void HandleCombat(float dist) // 함수를 선언할거에요 -> 거리별 전투 행동을 정하는 HandleCombat을 { - if (attackStyle == AttackStyle.Straight && dist < minDistance) RetreatFromPlayer(); - else if (dist <= attackRange) TryAttack(); - else { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); UpdateMovementAnimation(); } } + if (attackStyle == AttackStyle.Straight && dist < minDistance) RetreatFromPlayer(); // 조건이 맞으면 실행할거에요 -> 직선 공격 타입이고 너무 가까우면 도망가기를 + else if (dist <= attackRange) TryAttack(); // 조건이 맞으면 실행할거에요 -> 사거리 안이라면 공격 시도를 + else { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); UpdateMovementAnimation(); } } // 그 외엔 실행할거에요 -> 플레이어를 향해 추격하기를 } - void TryAttack() + void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을 { - if (Time.time < lastAttackTime + attackDelay) return; - lastAttackTime = Time.time; - isAttacking = true; + if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 쿨타임이 안 지났다면 + lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로 + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 - if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } + if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 이동을 멈추고 제자리에 서기를 - Vector3 lookDir = (playerTransform.position - transform.position).normalized; - lookDir.y = 0; - if (lookDir != Vector3.zero) transform.rotation = Quaternion.LookRotation(lookDir); + Vector3 lookDir = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 바라보는 방향을 + lookDir.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게 + if (lookDir != Vector3.zero) transform.rotation = Quaternion.LookRotation(lookDir); // 회전시킬거에요 -> 플레이어 쪽을 보도록 - animator.Play(attackAnim); + animator.Play(attackAnim); // 재생할거에요 -> 공격 애니메이션을 } - public override void OnAttackStart() + public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 타이밍(애니메이션 이벤트)에 호출될 OnAttackStart를 { - if (!projectilePrefab || !firePoint) return; + if (!projectilePrefab || !firePoint) return; // 조건이 맞으면 중단할거에요 -> 프리팹이나 발사 위치가 없다면 - GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation); + GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation); // 생성할거에요 -> 투사체를 발사 위치에 - if (obj.TryGetComponent(out var proj)) + if (obj.TryGetComponent(out var proj)) // 조건이 맞으면 실행할거에요 -> 투사체 스크립트가 있다면 { - float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f; - proj.Initialize(transform.forward, speed, attackDamage); + float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f; // 값을 결정할거에요 -> 직선이면 속도를, 곡사면 0(물리)을 + proj.Initialize(transform.forward, speed, attackDamage); // 초기화할거에요 -> 방향, 속도, 데미지 정보를 전달해서 } - if (attackStyle == AttackStyle.Lob) + if (attackStyle == AttackStyle.Lob) // 조건이 맞으면 실행할거에요 -> 곡사 공격 타입이라면 { - Rigidbody rb = obj.GetComponent(); - if (rb) + Rigidbody rb = obj.GetComponent(); // 컴포넌트를 가져올거에요 -> 투사체의 리지드바디를 + if (rb) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면 { - rb.useGravity = true; + rb.useGravity = true; // 기능을 켤거에요 -> 중력 영향을 받도록 // ⭐ 목표 지점 설정 (플레이어 위치 + 높이 보정) - Vector3 targetPos = playerTransform.position + Vector3.up * aimHeight; + Vector3 targetPos = playerTransform.position + Vector3.up * aimHeight; // 위치를 계산할거에요 -> 플레이어 위치에 조준 높이를 더해서 - if (usePreciseLob) + if (usePreciseLob) // 조건이 맞으면 실행할거에요 -> 정밀 곡사를 사용한다면 { // 보정된 위치(targetPos)를 기준으로 탄도 계산 - Vector3 velocity = CalculateLobVelocity(firePoint.position, targetPos, launchAngle); + Vector3 velocity = CalculateLobVelocity(firePoint.position, targetPos, launchAngle); // 계산할거에요 -> 목표에 도달하기 위한 물리 속도를 - if (!float.IsNaN(velocity.x)) + if (!float.IsNaN(velocity.x)) // 조건이 맞으면 실행할거에요 -> 계산 결과가 유효하다면 { - rb.velocity = velocity; + rb.velocity = velocity; // 값을 넣을거에요 -> 계산된 속도를 리지드바디에 } - else + else // 조건이 틀리면 실행할거에요 -> 계산 실패(도달 불가)라면 { // 계산 실패 시 백업 - rb.AddForce(transform.forward * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); + rb.AddForce(transform.forward * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 기본 힘으로 던지기를 } } - else + else // 조건이 틀리면 실행할거에요 -> 정밀 곡사가 아니라면 { - Vector3 dir = aimAtPlayer ? (targetPos - firePoint.position).normalized : transform.forward; - // dir.y = 0; // 높이 조준을 위해 y제거 주석 처리 (원하면 해제) - rb.AddForce(dir * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); + Vector3 dir = aimAtPlayer ? (targetPos - firePoint.position).normalized : transform.forward; // 방향을 결정할거에요 -> 타겟 방향 혹은 정면으로 + rb.AddForce(dir * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 방향과 힘을 적용해서 던지기를 } } - if (handModel != null) { handModel.SetActive(false); StartCoroutine(ReloadRoutine()); } + if (handModel != null) { handModel.SetActive(false); StartCoroutine(ReloadRoutine()); } // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 숨기고 장전 코루틴을 시작하기를 } } - Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle) + Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle) // 함수를 선언할거에요 -> 곡사 탄도 속도를 계산하는 CalculateLobVelocity를 { - Vector3 dir = target - origin; - float height = dir.y; - dir.y = 0; - float dist = dir.magnitude; + Vector3 dir = target - origin; // 벡터를 계산할거에요 -> 목표까지의 거리 벡터를 + float height = dir.y; // 값을 저장할거에요 -> 높이 차이를 + dir.y = 0; // 값을 바꿀거에요 -> 수평 거리 계산을 위해 y를 0으로 + float dist = dir.magnitude; // 값을 저장할거에요 -> 수평 거리를 - float a = angle * Mathf.Deg2Rad; + float a = angle * Mathf.Deg2Rad; // 값을 변환할거에요 -> 각도를 라디안으로 - dir.y = dist * Mathf.Tan(a); - dist += height / Mathf.Tan(a); + dir.y = dist * Mathf.Tan(a); // 값을 계산할거에요 -> 탄젠트를 이용해 높이를 + dist += height / Mathf.Tan(a); // 값을 보정할거에요 -> 높이 차이에 따른 거리 보정을 - float gravity = Physics.gravity.magnitude; - float velocitySq = (gravity * dist * dist) / (2 * Mathf.Pow(Mathf.Cos(a), 2) * (dist * Mathf.Tan(a) - height)); + float gravity = Physics.gravity.magnitude; // 값을 가져올거에요 -> 중력 가속도를 + // 물리 공식: v^2 = (g * x^2) / (2 * cos^2(a) * (x * tan(a) - y)) + float velocitySq = (gravity * dist * dist) / (2 * Mathf.Pow(Mathf.Cos(a), 2) * (dist * Mathf.Tan(a) - height)); // 값을 계산할거에요 -> 필요한 속도의 제곱을 - if (velocitySq <= 0) return Vector3.zero; + if (velocitySq <= 0) return Vector3.zero; // 조건이 맞으면 반환할거에요 -> 계산 불가라면 0 벡터를 - float velocity = Mathf.Sqrt(velocitySq); - return dir.normalized * velocity; + float velocity = Mathf.Sqrt(velocitySq); // 값을 계산할거에요 -> 제곱근을 구해서 실제 속도를 + return dir.normalized * velocity; // 반환할거에요 -> 방향 벡터에 속도를 곱해서 } - // ... (나머지 함수들은 기존과 동일) ... - void RetreatFromPlayer() + void RetreatFromPlayer() // 함수를 선언할거에요 -> 플레이어로부터 도망가는 RetreatFromPlayer를 { - if (agent.pathPending || (agent.remainingDistance > 0.5f && agent.velocity.magnitude > 0.1f)) { UpdateMovementAnimation(); return; } - Vector3 dir = (transform.position - playerTransform.position).normalized; - Vector3 pos = transform.position + dir * fleeDistance; - if (NavMesh.SamplePosition(pos, out NavMeshHit hit, 5f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } - UpdateMovementAnimation(); + if (agent.pathPending || (agent.remainingDistance > 0.5f && agent.velocity.magnitude > 0.1f)) { UpdateMovementAnimation(); return; } // 조건이 맞으면 중단할거에요 -> 이미 이동 중이라면 + Vector3 dir = (transform.position - playerTransform.position).normalized; // 벡터를 계산할거에요 -> 플레이어 반대 방향을 + Vector3 pos = transform.position + dir * fleeDistance; // 위치를 계산할거에요 -> 도망갈 목표 지점을 + if (NavMesh.SamplePosition(pos, out NavMeshHit hit, 5f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 그곳으로 이동하기를 + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션 갱신을 } - IEnumerator ReloadRoutine() + IEnumerator ReloadRoutine() // 코루틴 함수를 선언할거에요 -> 무기 장전(재생성)을 처리하는 ReloadRoutine을 { - isReloading = true; - yield return new WaitForSeconds(reloadTime); - if (handModel != null) handModel.SetActive(true); - isReloading = false; + isReloading = true; // 상태를 바꿀거에요 -> 장전 중 상태를 참으로 + yield return new WaitForSeconds(reloadTime); // 기다릴거에요 -> 장전 시간만큼 + if (handModel != null) handModel.SetActive(true); // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 다시 보이게 + isReloading = false; // 상태를 바꿀거에요 -> 장전 중 상태를 거짓으로 } - public override void OnAttackEnd() + public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를 { - isAttacking = false; - if (animator != null) animator.Play(Monster_Idle); - if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); + isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓으로 + if (animator != null) animator.Play(Monster_Idle); // 조건이 맞으면 실행할거에요 -> 대기 애니메이션으로 복귀를 + if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있다면 휴식 코루틴 시작을 } - void Patrol() + void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을 { - if (Time.time < nextPatrolTime) { if (agent.velocity.magnitude < 0.1f) animator.Play(Monster_Idle); return; } - Vector3 randomPoint = transform.position + new Vector3(Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius)); - if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } - nextPatrolTime = Time.time + patrolInterval; + if (Time.time < nextPatrolTime) { if (agent.velocity.magnitude < 0.1f) animator.Play(Monster_Idle); return; } // 조건이 맞으면 중단할거에요 -> 대기 시간이라면 대기 모션 재생 후 리턴 + Vector3 randomPoint = transform.position + new Vector3(Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius)); // 벡터를 계산할거에요 -> 랜덤 순찰 지점을 + if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 이동하기를 + nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을 } - void UpdateMovementAnimation() + void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션 갱신 함수를 { - if (isAttacking || isHit || isResting) return; - if (agent.velocity.magnitude > 0.1f) animator.Play(walkAnim); else animator.Play(Monster_Idle); + if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 + if (agent.velocity.magnitude > 0.1f) animator.Play(walkAnim); else animator.Play(Monster_Idle); // 조건에 따라 재생할거에요 -> 걷기 또는 대기 애니메이션을 } - - // private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } - //private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/Rangedbase.cs b/Assets/Scripts/Enemy/AI/Rangedbase.cs index 96e58059..d10ac71a 100644 --- a/Assets/Scripts/Enemy/AI/Rangedbase.cs +++ b/Assets/Scripts/Enemy/AI/Rangedbase.cs @@ -1,25 +1,25 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 /// /// 만능 투사체 (화살, 마법, 돌맹이 공용) /// -public class Projectile : MonoBehaviour +public class Projectile : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 Projectile을 { - [Header("기본 설정")] - [SerializeField] private float lifetime = 5f; // 수명 (5초 뒤 삭제) - [SerializeField] private GameObject hitEffectPrefab; // 맞으면 나오는 이펙트 (폭발 등) - [SerializeField] private bool destroyOnHit = true; // 맞으면 사라짐? (관통 아니면 true) + [Header("기본 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 기본 설정 을 + [SerializeField] private float lifetime = 5f; // 변수를 선언할거에요 -> 수명(5.0초)을 lifetime에 + [SerializeField] private GameObject hitEffectPrefab; // 변수를 선언할거에요 -> 충돌 이펙트 프리팹을 hitEffectPrefab에 + [SerializeField] private bool destroyOnHit = true; // 변수를 선언할거에요 -> 충돌 시 삭제 여부를 destroyOnHit에 // 내부 변수 - private Vector3 _direction; - private float _speed; - private float _damage; - private bool _isInitialized = false; - private Rigidbody _rigidbody; + private Vector3 _direction; // 변수를 선언할거에요 -> 날아갈 방향을 _direction에 + private float _speed; // 변수를 선언할거에요 -> 이동 속도를 _speed에 + private float _damage; // 변수를 선언할거에요 -> 공격 데미지를 _damage에 + private bool _isInitialized = false; // 변수를 초기화할거에요 -> 초기화 여부를 거짓(false)으로 + private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트 _rigidbody를 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를 { - _rigidbody = GetComponent(); + _rigidbody = GetComponent(); // 컴포넌트를 가져올거에요 -> 리지드바디를 } /// @@ -28,79 +28,79 @@ public class Projectile : MonoBehaviour /// 날아갈 방향 /// 속도 (0이면 물리력으로 날아감) /// 줄 데미지 - public void Initialize(Vector3 direction, float speed, float damage) + public void Initialize(Vector3 direction, float speed, float damage) // 함수를 선언할거에요 -> 투사체 정보를 설정하는 Initialize를 { - _direction = direction.normalized; - _speed = speed; - _damage = damage; - _isInitialized = true; + _direction = direction.normalized; // 값을 저장할거에요 -> 방향 벡터를 정규화해서 + _speed = speed; // 값을 저장할거에요 -> 속도를 + _damage = damage; // 값을 저장할거에요 -> 데미지를 + _isInitialized = true; // 상태를 바꿀거에요 -> 초기화 완료 상태를 참으로 // 속도가 있다? -> 화살/마법 (직선 운동) - if (_rigidbody != null && speed > 0) + if (_rigidbody != null && speed > 0) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있고 속도가 있다면 { - _rigidbody.velocity = _direction * _speed; - _rigidbody.useGravity = false; // 마법은 보통 중력 무시 + _rigidbody.velocity = _direction * _speed; // 값을 넣을거에요 -> 속도 벡터를 리지드바디 속도에 + _rigidbody.useGravity = false; // 설정을 바꿀거에요 -> 중력 영향을 끄기로 (직선 비행) } // 속도가 0이다? -> 돌맹이 (외부에서 물리력 가함) - else if (_rigidbody != null && speed == 0) + else if (_rigidbody != null && speed == 0) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있고 속도가 0이라면 { - _rigidbody.useGravity = true; // 돌은 중력 받아야 함 + _rigidbody.useGravity = true; // 설정을 바꿀거에요 -> 중력 영향을 켜기로 (포물선 비행) } // 수명 지나면 자동 삭제 - Destroy(gameObject, lifetime); + Destroy(gameObject, lifetime); // 파괴할거에요 -> 이 오브젝트를 수명 시간이 지나면 } - private void FixedUpdate() + private void FixedUpdate() // 함수를 실행할거에요 -> 물리 연산 주기로 호출되는 FixedUpdate를 { - if (!_isInitialized) return; + if (!_isInitialized) return; // 조건이 맞으면 중단할거에요 -> 초기화되지 않았다면 // 리지드바디가 없거나, Kinematic인 경우 (강제 이동 보정) - if (_rigidbody == null || _rigidbody.isKinematic) + if (_rigidbody == null || _rigidbody.isKinematic) // 조건이 맞으면 실행할거에요 -> 물리 이동을 안 쓰는 상태라면 { - if (_speed > 0) + if (_speed > 0) // 조건이 맞으면 실행할거에요 -> 속도가 설정되어 있다면 { - transform.position += _direction * _speed * Time.fixedDeltaTime; + transform.position += _direction * _speed * Time.fixedDeltaTime; // 이동시킬거에요 -> 방향과 속도에 맞춰 직접 위치를 } } } - private void OnTriggerEnter(Collider other) + private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 충돌이 발생했을 때 OnTriggerEnter를 { // 1. 적(몬스터)끼리 맞으면 무시 - if (other.CompareTag("Enemy") || other.gameObject == gameObject) return; + if (other.CompareTag("Enemy") || other.gameObject == gameObject) return; // 조건이 맞으면 중단할거에요 -> 적끼리 충돌했거나 자기 자신이라면 // 2. 투사체끼리 충돌 무시 - if (other.GetComponent()) return; + if (other.GetComponent()) return; // 조건이 맞으면 중단할거에요 -> 다른 투사체와 충돌했다면 // 3. 플레이어 피격 처리 - if (other.CompareTag("Player")) + if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌 대상이 플레이어라면 { // PlayerHealth 혹은 IDamageable 스크립트 찾기 - var playerHealth = other.GetComponent(); + var playerHealth = other.GetComponent(); // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를 - if (playerHealth != null) + if (playerHealth != null) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 { // 무적 상태 체크 (있다면) - if (!playerHealth.isInvincible) + if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 플레이어가 무적 상태가 아니라면 { - playerHealth.TakeDamage(_damage); - Debug.Log($"🎯 [적중] 플레이어에게 {_damage} 데미지!"); + playerHealth.TakeDamage(_damage); // 함수를 실행할거에요 -> 데미지를 입히는 TakeDamage를 + Debug.Log($"🎯 [적중] 플레이어에게 {_damage} 데미지!"); // 로그를 출력할거에요 -> 적중 메시지를 } } } // 4. 벽이나 땅에 닿았을 때도 이펙트 - if (other.CompareTag("Ground") || other.CompareTag("Wall") || other.CompareTag("Player")) + if (other.CompareTag("Ground") || other.CompareTag("Wall") || other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 땅, 벽, 플레이어에 닿았다면 { - if (hitEffectPrefab != null) + if (hitEffectPrefab != null) // 조건이 맞으면 실행할거에요 -> 충돌 이펙트 프리팹이 있다면 { - Instantiate(hitEffectPrefab, transform.position, Quaternion.identity); + Instantiate(hitEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 이펙트를 충돌 위치에 } - if (destroyOnHit) + if (destroyOnHit) // 조건이 맞으면 실행할거에요 -> 충돌 시 삭제 옵션이 켜져 있다면 { - Destroy(gameObject); + Destroy(gameObject); // 파괴할거에요 -> 이 투사체 오브젝트를 } } } diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs b/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs index fdd6bd33..b951280d 100644 --- a/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs +++ b/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs @@ -1,77 +1,75 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 /// /// 보스 카운터 시스템의 임계치, 가중치, 감소(Decay) 설정을 담는 ScriptableObject. /// Inspector에서 밸런싱을 위해 쉽게 조정할 수 있습니다. -/// -/// [사용법] Project 창에서 우클릭 → Create → Boss/Counter Config /// -[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/Counter Config")] -public class BossCounterConfig : ScriptableObject +[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/Counter Config")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "Boss/Counter Config"를 +public class BossCounterConfig : ScriptableObject // 클래스를 선언할거에요 -> ScriptableObject를 상속받는 BossCounterConfig를 { - [Header("══ 기본 임계치 (첫 런 / 잠금 해제 전) ══")] - [Tooltip("회피 카운터 발동 조건: 10초 내 회피 횟수")] - public int dodgeThreshold = 5; + [Header("══ 기본 임계치 (첫 런 / 잠금 해제 전) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 기본 임계치 (첫 런 / 잠금 해제 전) ══ 을 + [Tooltip("회피 카운터 발동 조건: 10초 내 회피 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public int dodgeThreshold = 5; // 변수를 선언할거에요 -> 회피 횟수 임계값(5)을 dodgeThreshold에 - [Tooltip("조준 카운터 발동 조건: 10초 내 조준 유지 시간(초)")] - public float aimThreshold = 4.0f; + [Tooltip("조준 카운터 발동 조건: 10초 내 조준 유지 시간(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public float aimThreshold = 4.0f; // 변수를 선언할거에요 -> 조준 시간 임계값(4.0초)을 aimThreshold에 - [Tooltip("관통 카운터 발동 조건: 10초 내 관통 비율")] - [Range(0f, 1f)] - public float pierceThreshold = 0.6f; + [Tooltip("관통 카운터 발동 조건: 10초 내 관통 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0f, 1f)] // 슬라이더를 표시할거에요 -> 0부터 1 사이로 + public float pierceThreshold = 0.6f; // 변수를 선언할거에요 -> 관통 비율 임계값(60%)을 pierceThreshold에 - [Tooltip("관통 비율 판단을 위한 최소 발사 횟수")] - public int minShotsForPierceCheck = 3; + [Tooltip("관통 비율 판단을 위한 최소 발사 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public int minShotsForPierceCheck = 3; // 변수를 선언할거에요 -> 최소 발사 횟수(3)를 minShotsForPierceCheck에 - [Header("══ 잠금 해제 후 임계치 감소 ══")] - [Tooltip("잠금 해제된 카운터의 임계치 감소 비율 (0.8 = 20% 낮아짐)")] - [Range(0.5f, 1.0f)] - public float unlockedThresholdMultiplier = 0.8f; + [Header("══ 잠금 해제 후 임계치 감소 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 잠금 해제 후 임계치 감소 ══ 를 + [Tooltip("잠금 해제된 카운터의 임계치 감소 비율 (0.8 = 20% 낮아짐)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0.5f, 1.0f)] // 슬라이더를 표시할거에요 -> 0.5부터 1.0 사이로 + public float unlockedThresholdMultiplier = 0.8f; // 변수를 선언할거에요 -> 잠금 해제 시 감소 배율(0.8)을 unlockedThresholdMultiplier에 - [Header("══ 가중치 설정 (확률 가중치 방식) ══")] - [Tooltip("카운터 발동 시 해당 카운터 패턴에 추가되는 가중치")] - public float counterWeightBonus = 3f; + [Header("══ 가중치 설정 (확률 가중치 방식) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 가중치 설정 (확률 가중치 방식) ══ 을 + [Tooltip("카운터 발동 시 해당 카운터 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public float counterWeightBonus = 3f; // 변수를 선언할거에요 -> 카운터 패턴 보너스 가중치(3.0)를 counterWeightBonus에 - [Tooltip("카운터 발동 시 보조 패턴에 추가되는 가중치")] - public float counterSubWeightBonus = 2f; + [Tooltip("카운터 발동 시 보조 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public float counterSubWeightBonus = 2f; // 변수를 선언할거에요 -> 보조 패턴 보너스 가중치(2.0)를 counterSubWeightBonus에 - [Header("══ Decay(감소) 설정 ══")] - [Tooltip("카운터 모드 유지 시간(초). 이 시간 동안 조건 미충족 시 비활성화")] - public float counterDecayTime = 8f; + [Header("══ Decay(감소) 설정 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ Decay(감소) 설정 ══ 을 + [Tooltip("카운터 모드 유지 시간(초). 이 시간 동안 조건 미충족 시 비활성화")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public float counterDecayTime = 8f; // 변수를 선언할거에요 -> 카운터 유지 시간(8.0초)을 counterDecayTime에 - [Header("══ 빈도 제한 ══")] - [Tooltip("카운터 패턴 선택 확률 상한 (0.25 = 최대 25%)")] - [Range(0.1f, 0.5f)] - public float maxCounterFrequency = 0.25f; + [Header("══ 빈도 제한 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 빈도 제한 ══ 을 + [Tooltip("카운터 패턴 선택 확률 상한 (0.25 = 최대 25%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0.1f, 0.5f)] // 슬라이더를 표시할거에요 -> 0.1부터 0.5 사이로 + public float maxCounterFrequency = 0.25f; // 변수를 선언할거에요 -> 카운터 최대 빈도(25%)를 maxCounterFrequency에 - [Tooltip("카운터 패턴 발동 후 쿨타임(초)")] - public float counterCooldown = 5f; + [Tooltip("카운터 패턴 발동 후 쿨타임(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public float counterCooldown = 5f; // 변수를 선언할거에요 -> 카운터 재사용 대기시간(5.0초)을 counterCooldown에 - [Header("══ 습관 변경 보상 ══")] - [Tooltip("플레이어가 이전 런에서 발동된 카운터 습관을 이번 런에서 안 보이면, 보스 일반 패턴 난이도 감소 비율")] - [Range(0f, 0.3f)] - public float habitChangeRewardRatio = 0.15f; + [Header("══ 습관 변경 보상 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 습관 변경 보상 ══ 을 + [Tooltip("플레이어가 이전 런에서 발동된 카운터 습관을 이번 런에서 안 보이면, 보스 일반 패턴 난이도 감소 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0f, 0.3f)] // 슬라이더를 표시할거에요 -> 0부터 0.3 사이로 + public float habitChangeRewardRatio = 0.15f; // 변수를 선언할거에요 -> 습관 변경 보상 비율(15%)을 habitChangeRewardRatio에 // ═══════════════════════════════════════════ // 임계치 조회 (잠금 해제 여부 반영) // ═══════════════════════════════════════════ /// 현재 유효 임계치 반환 (잠금 해제 시 낮아짐) - public int GetEffectiveDodgeThreshold(bool isUnlocked) + public int GetEffectiveDodgeThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 회피 임계값을 반환하는 GetEffectiveDodgeThreshold를 { - if (!isUnlocked) return dodgeThreshold; - return Mathf.Max(2, Mathf.RoundToInt(dodgeThreshold * unlockedThresholdMultiplier)); + if (!isUnlocked) return dodgeThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을 + return Mathf.Max(2, Mathf.RoundToInt(dodgeThreshold * unlockedThresholdMultiplier)); // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱하고 최소 2 이상이 되도록 } - public float GetEffectiveAimThreshold(bool isUnlocked) + public float GetEffectiveAimThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 조준 임계값을 반환하는 GetEffectiveAimThreshold를 { - if (!isUnlocked) return aimThreshold; - return aimThreshold * unlockedThresholdMultiplier; + if (!isUnlocked) return aimThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을 + return aimThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을 } - public float GetEffectivePierceThreshold(bool isUnlocked) + public float GetEffectivePierceThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 관통 임계값을 반환하는 GetEffectivePierceThreshold를 { - if (!isUnlocked) return pierceThreshold; - return pierceThreshold * unlockedThresholdMultiplier; + if (!isUnlocked) return pierceThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을 + return pierceThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을 } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs b/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs index ca6f380f..f4c8088f 100644 --- a/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs +++ b/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs @@ -1,48 +1,49 @@ -using System.Collections; -using UnityEngine; -using UnityEngine.UI; -using TMPro; +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를 +using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를 /// /// 보스 카운터 시스템의 연출/UI를 담당합니다. -/// 카운터 발동 시 보스 대사, 화면 효과 등을 처리합니다. -/// -/// "...또 그 수법이냐" 같은 대사로 "보스가 기억한다"는 서사 전달. /// -public class BossCounterFeedback : MonoBehaviour +public class BossCounterFeedback : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterFeedback을 { - [Header("참조")] - [SerializeField] private BossCounterSystem counterSystem; + [Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를 + [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 카운터 시스템 스크립트를 연결할 counterSystem을 - [Header("UI 요소 (선택)")] - [SerializeField] private TextMeshProUGUI bossDialogueText; - [SerializeField] private CanvasGroup dialogueCanvasGroup; - [SerializeField] private float dialogueDisplayDuration = 3f; - [SerializeField] private float dialogueFadeDuration = 0.5f; + [Header("UI 요소 (선택)")] // 인스펙터 창에 제목을 표시할거에요 -> UI 요소 (선택) 을 + [SerializeField] private TextMeshProUGUI bossDialogueText; // 변수를 선언할거에요 -> 보스 대사를 표시할 텍스트 UI를 + [SerializeField] private CanvasGroup dialogueCanvasGroup; // 변수를 선언할거에요 -> 대사창의 투명도를 조절할 캔버스 그룹을 + [SerializeField] private float dialogueDisplayDuration = 3f; // 변수를 선언할거에요 -> 대사 표시 시간(3초)을 dialogueDisplayDuration에 + [SerializeField] private float dialogueFadeDuration = 0.5f; // 변수를 선언할거에요 -> 대사 페이드 시간(0.5초)을 dialogueFadeDuration에 - [Header("카운터별 보스 대사")] - [SerializeField] private string[] dodgeCounterDialogues = new string[] + [Header("카운터별 보스 대사")] // 인스펙터 창에 제목을 표시할거에요 -> 카운터별 보스 대사 를 + [SerializeField] + private string[] dodgeCounterDialogues = new string[] // 배열을 초기화할거에요 -> 회피 카운터 대사 목록을 { "...또 도망치려는 건가.", "네 발은 이미 읽었다.", "아무리 피해도 소용없어." }; - [SerializeField] private string[] aimCounterDialogues = new string[] + [SerializeField] + private string[] aimCounterDialogues = new string[] // 배열을 초기화할거에요 -> 조준 카운터 대사 목록을 { "그렇게 오래 노려봐야...", "느린 조준은 빈틈이지.", "시간을 줄 생각은 없다." }; - [SerializeField] private string[] pierceCounterDialogues = new string[] + [SerializeField] + private string[] pierceCounterDialogues = new string[] // 배열을 초기화할거에요 -> 관통 카운터 대사 목록을 { "그 화살은 더 이상 통하지 않아.", "관통? 이번엔 막아주지.", "같은 수를 반복하다니." }; - [SerializeField] private string[] habitChangeDialogues = new string[] + [SerializeField] + private string[] habitChangeDialogues = new string[] // 배열을 초기화할거에요 -> 습관 변경 보상 대사 목록을 { "...다른 수를 쓰는 건가.", "흥, 조금은 배운 모양이군.", @@ -50,33 +51,34 @@ public class BossCounterFeedback : MonoBehaviour }; // ── 잠금 해제 여부에 따른 첫 발동 대사 ── - [Header("첫 잠금 해제 시 대사 (서사 강조)")] - [SerializeField] private string[] firstUnlockDialogues = new string[] + [Header("첫 잠금 해제 시 대사 (서사 강조)")] // 인스펙터 창에 제목을 표시할거에요 -> 첫 잠금 해제 시 대사 (서사 강조) 를 + [SerializeField] + private string[] firstUnlockDialogues = new string[] // 배열을 초기화할거에요 -> 첫 해금 시 출력할 대사 목록을 { "...기억했다. 네 버릇을.", "이제 알겠어. 네 전투 방식이.", "한 번이면 충분해. 패턴을 읽었다." }; - private Coroutine dialogueCoroutine; + private Coroutine dialogueCoroutine; // 변수를 선언할거에요 -> 실행 중인 대사 코루틴을 저장할 dialogueCoroutine을 - private void OnEnable() + private void OnEnable() // 함수를 실행할거에요 -> 오브젝트 활성화 시 호출되는 OnEnable을 { - if (counterSystem != null) + if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면 { - counterSystem.OnCounterActivated.AddListener(OnCounterActivated); - counterSystem.OnCounterDeactivated.AddListener(OnCounterDeactivated); - counterSystem.OnHabitChangeRewarded.AddListener(OnHabitChangeRewarded); + counterSystem.OnCounterActivated.AddListener(OnCounterActivated); // 구독할거에요 -> 카운터 활성 이벤트에 OnCounterActivated를 + counterSystem.OnCounterDeactivated.AddListener(OnCounterDeactivated); // 구독할거에요 -> 카운터 비활성 이벤트에 OnCounterDeactivated를 + counterSystem.OnHabitChangeRewarded.AddListener(OnHabitChangeRewarded); // 구독할거에요 -> 보상 이벤트에 OnHabitChangeRewarded를 } } - private void OnDisable() + private void OnDisable() // 함수를 실행할거에요 -> 오브젝트 비활성화 시 호출되는 OnDisable을 { - if (counterSystem != null) + if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면 { - counterSystem.OnCounterActivated.RemoveListener(OnCounterActivated); - counterSystem.OnCounterDeactivated.RemoveListener(OnCounterDeactivated); - counterSystem.OnHabitChangeRewarded.RemoveListener(OnHabitChangeRewarded); + counterSystem.OnCounterActivated.RemoveListener(OnCounterActivated); // 구독 해제할거에요 -> 카운터 활성 이벤트를 + counterSystem.OnCounterDeactivated.RemoveListener(OnCounterDeactivated); // 구독 해제할거에요 -> 카운터 비활성 이벤트를 + counterSystem.OnHabitChangeRewarded.RemoveListener(OnHabitChangeRewarded); // 구독 해제할거에요 -> 보상 이벤트를 } } @@ -84,106 +86,100 @@ public class BossCounterFeedback : MonoBehaviour // 이벤트 핸들러 // ═══════════════════════════════════════════ - private void OnCounterActivated(CounterType type) + private void OnCounterActivated(CounterType type) // 함수를 선언할거에요 -> 카운터 활성 시 호출될 OnCounterActivated를 { - var persistence = BossCounterPersistence.Instance; + var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에 // 첫 잠금 해제인지 확인 (총 발동 횟수가 1이면 방금 처음 잠금 해제된 것) - bool isFirstUnlock = false; - if (persistence != null) + bool isFirstUnlock = false; // 변수를 초기화할거에요 -> 첫 해금 여부를 거짓(false)으로 + if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면 { - int activations = type switch + int activations = type switch // 값을 가져올거에요 -> 타입에 따른 발동 횟수를 { CounterType.Dodge => persistence.Data.dodgeCounterActivations, CounterType.Aim => persistence.Data.aimCounterActivations, CounterType.Pierce => persistence.Data.pierceCounterActivations, _ => 0 }; - isFirstUnlock = (activations <= 1); + isFirstUnlock = (activations <= 1); // 판단할거에요 -> 발동 횟수가 1 이하라면 첫 해금이라고 } // 대사 선택 - string dialogue; - if (isFirstUnlock) + string dialogue; // 변수를 선언할거에요 -> 출력할 대사를 담을 dialogue를 + if (isFirstUnlock) // 조건이 맞으면 실행할거에요 -> 첫 해금이라면 { - dialogue = firstUnlockDialogues[Random.Range(0, firstUnlockDialogues.Length)]; + dialogue = firstUnlockDialogues[Random.Range(0, firstUnlockDialogues.Length)]; // 선택할거에요 -> 첫 해금 대사 중 하나를 랜덤으로 } - else + else // 조건이 틀리면 실행할거에요 -> 이미 해금된 상태라면 { - dialogue = type switch + dialogue = type switch // 분기할거에요 -> 카운터 타입에 따라 { - CounterType.Dodge => dodgeCounterDialogues[Random.Range(0, dodgeCounterDialogues.Length)], - CounterType.Aim => aimCounterDialogues[Random.Range(0, aimCounterDialogues.Length)], - CounterType.Pierce => pierceCounterDialogues[Random.Range(0, pierceCounterDialogues.Length)], - _ => "" + CounterType.Dodge => dodgeCounterDialogues[Random.Range(0, dodgeCounterDialogues.Length)], // 선택할거에요 -> 회피 대사를 + CounterType.Aim => aimCounterDialogues[Random.Range(0, aimCounterDialogues.Length)], // 선택할거에요 -> 조준 대사를 + CounterType.Pierce => pierceCounterDialogues[Random.Range(0, pierceCounterDialogues.Length)], // 선택할거에요 -> 관통 대사를 + _ => "" // 기본값은 빈 문자열로 }; } - ShowDialogue(dialogue); + ShowDialogue(dialogue); // 함수를 실행할거에요 -> 선택된 대사를 화면에 띄우는 ShowDialogue를 - // TODO: 여기에 추가 연출 넣기 - // - 보스 전용 애니메이션 트리거 (예: animator.SetTrigger("CounterReady")) - // - 보스 주변 이펙트 (파티클, 오라 등) - // - 카메라 연출 (줌인, 슬로모션 등) - // - 사운드 효과 - Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 재생" + - (isFirstUnlock ? " (첫 잠금 해제!)" : "")); + Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 재생" + (isFirstUnlock ? " (첫 잠금 해제!)" : "")); // 로그를 출력할거에요 -> 연출 재생 알림을 } - private void OnCounterDeactivated(CounterType type) + private void OnCounterDeactivated(CounterType type) // 함수를 선언할거에요 -> 카운터 종료 시 호출될 OnCounterDeactivated를 { // 카운터 해제 시 연출 (이펙트 종료 등) - Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 종료"); + Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 종료"); // 로그를 출력할거에요 -> 연출 종료 알림을 } - private void OnHabitChangeRewarded() + private void OnHabitChangeRewarded() // 함수를 선언할거에요 -> 보상 지급 시 호출될 OnHabitChangeRewarded를 { - string dialogue = habitChangeDialogues[Random.Range(0, habitChangeDialogues.Length)]; - ShowDialogue(dialogue); + string dialogue = habitChangeDialogues[Random.Range(0, habitChangeDialogues.Length)]; // 선택할거에요 -> 보상 대사 중 하나를 랜덤으로 + ShowDialogue(dialogue); // 함수를 실행할거에요 -> 대사를 띄우는 ShowDialogue를 - Debug.Log("[BossCounterFeedback] 습관 변경 보상 연출!"); + Debug.Log("[BossCounterFeedback] 습관 변경 보상 연출!"); // 로그를 출력할거에요 -> 보상 연출 알림을 } // ═══════════════════════════════════════════ // 대사 표시 // ═══════════════════════════════════════════ - private void ShowDialogue(string text) + private void ShowDialogue(string text) // 함수를 선언할거에요 -> 텍스트를 UI에 표시하는 ShowDialogue를 { - if (bossDialogueText == null || dialogueCanvasGroup == null) return; + if (bossDialogueText == null || dialogueCanvasGroup == null) return; // 조건이 맞으면 중단할거에요 -> UI 요소가 연결되지 않았다면 - if (dialogueCoroutine != null) - StopCoroutine(dialogueCoroutine); + if (dialogueCoroutine != null) // 조건이 맞으면 실행할거에요 -> 이미 실행 중인 대사가 있다면 + StopCoroutine(dialogueCoroutine); // 중단할거에요 -> 기존 코루틴을 - dialogueCoroutine = StartCoroutine(DialogueRoutine(text)); + dialogueCoroutine = StartCoroutine(DialogueRoutine(text)); // 코루틴을 시작할거에요 -> 새 대사를 출력하는 DialogueRoutine을 } - private IEnumerator DialogueRoutine(string text) + private IEnumerator DialogueRoutine(string text) // 코루틴 함수를 선언할거에요 -> 페이드 효과와 함께 대사를 출력하는 DialogueRoutine을 { - bossDialogueText.text = text; - dialogueCanvasGroup.alpha = 0f; + bossDialogueText.text = text; // 값을 설정할거에요 -> UI 텍스트 내용을 전달받은 text로 + dialogueCanvasGroup.alpha = 0f; // 값을 설정할거에요 -> 투명도를 0(안 보임)으로 // 페이드 인 - float t = 0f; - while (t < dialogueFadeDuration) + float t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로 + while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안 { - t += Time.deltaTime; - dialogueCanvasGroup.alpha = t / dialogueFadeDuration; - yield return null; + t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을 + dialogueCanvasGroup.alpha = t / dialogueFadeDuration; // 값을 조절할거에요 -> 투명도를 서서히 1로 + yield return null; // 대기할거에요 -> 다음 프레임까지 } - dialogueCanvasGroup.alpha = 1f; + dialogueCanvasGroup.alpha = 1f; // 값을 확정할거에요 -> 투명도를 완전 불투명(1)으로 // 유지 - yield return new WaitForSeconds(dialogueDisplayDuration); + yield return new WaitForSeconds(dialogueDisplayDuration); // 기다릴거에요 -> 대사 유지 시간만큼 // 페이드 아웃 - t = 0f; - while (t < dialogueFadeDuration) + t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 다시 0으로 + while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안 { - t += Time.deltaTime; - dialogueCanvasGroup.alpha = 1f - (t / dialogueFadeDuration); - yield return null; + t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을 + dialogueCanvasGroup.alpha = 1f - (t / dialogueFadeDuration); // 값을 조절할거에요 -> 투명도를 서서히 0으로 + yield return null; // 대기할거에요 -> 다음 프레임까지 } - dialogueCanvasGroup.alpha = 0f; + dialogueCanvasGroup.alpha = 0f; // 값을 확정할거에요 -> 투명도를 완전 투명(0)으로 } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs b/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs index d75ca940..80aeed41 100644 --- a/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs +++ b/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs @@ -1,50 +1,40 @@ -using System.Collections.Generic; -using UnityEngine; +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 /// /// 런 간 영구 저장되는 보스 카운터 잠금 해제 데이터. -/// "보스가 플레이어를 기억한다"는 서사를 담당합니다. -/// -/// 핵심 규칙: -/// - 잠금 해제는 영구 (런이 끝나도 유지) -/// - 발동은 조건부 (해당 런에서 플레이어가 습관을 보여야만 활성화) -/// - 잠금 해제된 카운터는 임계치가 약간 낮아짐 (보스가 "이미 알고 있으니 더 빨리 눈치챔") /// -[System.Serializable] -public class BossCounterSaveData +[System.Serializable] // 직렬화할거에요 -> 인스펙터나 JSON으로 저장 가능하게 +public class BossCounterSaveData // 클래스를 선언할거에요 -> 저장할 데이터 구조체인 BossCounterSaveData를 { - public bool dodgeCounterUnlocked = false; - public bool aimCounterUnlocked = false; - public bool pierceCounterUnlocked = false; + public bool dodgeCounterUnlocked = false; // 변수를 선언할거에요 -> 회피 카운터 해금 여부를 + public bool aimCounterUnlocked = false; // 변수를 선언할거에요 -> 조준 카운터 해금 여부를 + public bool pierceCounterUnlocked = false; // 변수를 선언할거에요 -> 관통 카운터 해금 여부를 - /// 각 카운터가 발동된 총 횟수 (연출/난이도 스케일링에 활용 가능) - public int dodgeCounterActivations = 0; - public int aimCounterActivations = 0; - public int pierceCounterActivations = 0; + /// 각 카운터가 발동된 총 횟수 + public int dodgeCounterActivations = 0; // 변수를 선언할거에요 -> 회피 카운터 총 발동 횟수를 + public int aimCounterActivations = 0; // 변수를 선언할거에요 -> 조준 카운터 총 발동 횟수를 + public int pierceCounterActivations = 0; // 변수를 선언할거에요 -> 관통 카운터 총 발동 횟수를 } -/// -/// 보스 카운터 영구 데이터를 관리합니다. -/// PlayerPrefs 기반으로 런 간 저장/로드합니다. -/// -public class BossCounterPersistence : MonoBehaviour +public class BossCounterPersistence : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterPersistence를 { - public static BossCounterPersistence Instance { get; private set; } + public static BossCounterPersistence Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를 - private const string SAVE_KEY = "BossCounterData"; + private const string SAVE_KEY = "BossCounterData"; // 상수를 정의할거에요 -> 저장에 사용할 키 이름("BossCounterData")을 SAVE_KEY에 - public BossCounterSaveData Data { get; private set; } + public BossCounterSaveData Data { get; private set; } // 프로퍼티를 선언할거에요 -> 실제 저장 데이터를 담을 Data를 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를 { - if (Instance != null && Instance != this) + if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면 { - Destroy(gameObject); - return; + Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를 + return; // 중단할거에요 -> 초기화 로직을 } - Instance = this; - DontDestroyOnLoad(gameObject); - Load(); + Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에 + DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬이 바뀌어도 이 오브젝트를 파괴하지 않게 + Load(); // 함수를 실행할거에요 -> 저장된 데이터를 불러오는 Load를 } // ═══════════════════════════════════════════ @@ -52,88 +42,88 @@ public class BossCounterPersistence : MonoBehaviour // ═══════════════════════════════════════════ /// 특정 카운터 타입을 영구 잠금 해제 - public void UnlockCounter(CounterType type) + public void UnlockCounter(CounterType type) // 함수를 선언할거에요 -> 카운터를 해금하는 UnlockCounter를 { - switch (type) + switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라 { - case CounterType.Dodge: - if (!Data.dodgeCounterUnlocked) + case CounterType.Dodge: // 조건이 맞으면 실행할거에요 -> 회피 타입이라면 + if (!Data.dodgeCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면 { - Data.dodgeCounterUnlocked = true; - Debug.Log("[BossCounter] 회피 카운터 영구 잠금 해제!"); + Data.dodgeCounterUnlocked = true; // 값을 바꿀거에요 -> 회피 해금 상태를 참(true)으로 + Debug.Log("[BossCounter] 회피 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를 } break; - case CounterType.Aim: - if (!Data.aimCounterUnlocked) + case CounterType.Aim: // 조건이 맞으면 실행할거에요 -> 조준 타입이라면 + if (!Data.aimCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면 { - Data.aimCounterUnlocked = true; - Debug.Log("[BossCounter] 조준 카운터 영구 잠금 해제!"); + Data.aimCounterUnlocked = true; // 값을 바꿀거에요 -> 조준 해금 상태를 참(true)으로 + Debug.Log("[BossCounter] 조준 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를 } break; - case CounterType.Pierce: - if (!Data.pierceCounterUnlocked) + case CounterType.Pierce: // 조건이 맞으면 실행할거에요 -> 관통 타입이라면 + if (!Data.pierceCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면 { - Data.pierceCounterUnlocked = true; - Debug.Log("[BossCounter] 관통 카운터 영구 잠금 해제!"); + Data.pierceCounterUnlocked = true; // 값을 바꿀거에요 -> 관통 해금 상태를 참(true)으로 + Debug.Log("[BossCounter] 관통 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를 } break; } - Save(); + Save(); // 함수를 실행할거에요 -> 변경된 데이터를 저장하는 Save를 } /// 카운터가 잠금 해제되어 있는지 확인 - public bool IsUnlocked(CounterType type) + public bool IsUnlocked(CounterType type) // 함수를 선언할거에요 -> 해금 여부를 확인하는 IsUnlocked를 { - return type switch + return type switch // 반환할거에요 -> 타입에 따른 해금 변수 값을 { - CounterType.Dodge => Data.dodgeCounterUnlocked, - CounterType.Aim => Data.aimCounterUnlocked, - CounterType.Pierce => Data.pierceCounterUnlocked, - _ => false + CounterType.Dodge => Data.dodgeCounterUnlocked, // 매칭되면 반환할거에요 -> 회피 해금 여부를 + CounterType.Aim => Data.aimCounterUnlocked, // 매칭되면 반환할거에요 -> 조준 해금 여부를 + CounterType.Pierce => Data.pierceCounterUnlocked, // 매칭되면 반환할거에요 -> 관통 해금 여부를 + _ => false // 그 외에는 반환할거에요 -> 거짓(false)을 }; } /// 카운터 발동 횟수 기록 - public void RecordActivation(CounterType type) + public void RecordActivation(CounterType type) // 함수를 선언할거에요 -> 발동 횟수를 기록하는 RecordActivation을 { - switch (type) + switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라 { - case CounterType.Dodge: Data.dodgeCounterActivations++; break; - case CounterType.Aim: Data.aimCounterActivations++; break; - case CounterType.Pierce: Data.pierceCounterActivations++; break; + case CounterType.Dodge: Data.dodgeCounterActivations++; break; // 일치하면 실행할거에요 -> 회피 발동 횟수를 1 증가시키기를 + case CounterType.Aim: Data.aimCounterActivations++; break; // 일치하면 실행할거에요 -> 조준 발동 횟수를 1 증가시키기를 + case CounterType.Pierce: Data.pierceCounterActivations++; break; // 일치하면 실행할거에요 -> 관통 발동 횟수를 1 증가시키기를 } - Save(); + Save(); // 함수를 실행할거에요 -> 변경된 횟수를 저장하는 Save를 } // ═══════════════════════════════════════════ // 저장 / 로드 / 리셋 // ═══════════════════════════════════════════ - public void Save() + public void Save() // 함수를 선언할거에요 -> 데이터를 디스크에 저장하는 Save를 { - string json = JsonUtility.ToJson(Data); - PlayerPrefs.SetString(SAVE_KEY, json); - PlayerPrefs.Save(); + string json = JsonUtility.ToJson(Data); // 변환할거에요 -> 데이터 객체를 JSON 문자열로 + PlayerPrefs.SetString(SAVE_KEY, json); // 저장할거에요 -> JSON 문자열을 PlayerPrefs에 + PlayerPrefs.Save(); // 실행할거에요 -> 변경사항을 디스크에 즉시 쓰기를 } - public void Load() + public void Load() // 함수를 선언할거에요 -> 데이터를 불러오는 Load를 { - if (PlayerPrefs.HasKey(SAVE_KEY)) + if (PlayerPrefs.HasKey(SAVE_KEY)) // 조건이 맞으면 실행할거에요 -> 저장된 키가 존재한다면 { - string json = PlayerPrefs.GetString(SAVE_KEY); - Data = JsonUtility.FromJson(json); + string json = PlayerPrefs.GetString(SAVE_KEY); // 불러올거에요 -> 저장된 JSON 문자열을 + Data = JsonUtility.FromJson(json); // 변환할거에요 -> JSON을 데이터 객체로 복구해서 Data에 } - else + else // 조건이 틀리면 실행할거에요 -> 저장된 데이터가 없다면 { - Data = new BossCounterSaveData(); + Data = new BossCounterSaveData(); // 생성할거에요 -> 새로운 빈 데이터 객체를 } } /// 모든 영구 데이터 초기화 (디버그/뉴게임+) - public void ResetAllData() + public void ResetAllData() // 함수를 선언할거에요 -> 모든 데이터를 초기화하는 ResetAllData를 { - Data = new BossCounterSaveData(); - PlayerPrefs.DeleteKey(SAVE_KEY); - Debug.Log("[BossCounter] 모든 보스 기억 데이터 초기화됨"); + Data = new BossCounterSaveData(); // 초기화할거에요 -> 데이터 객체를 새 것으로 + PlayerPrefs.DeleteKey(SAVE_KEY); // 삭제할거에요 -> 저장된 키를 + Debug.Log("[BossCounter] 모든 보스 기억 데이터 초기화됨"); // 로그를 출력할거에요 -> 초기화 완료 메시지를 } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs b/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs index be1a3ef5..7775a9cb 100644 --- a/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs +++ b/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs @@ -1,213 +1,206 @@ -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.Events; +using System.Collections.Generic; // 리스트와 딕셔너리 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.Events; // 유니티 이벤트 기능을 사용할거에요 -> UnityEngine.Events를 /// /// 보스 카운터 시스템 메인 컨트롤러. /// (수정됨: Config나 Player가 없어도 에러가 나지 않도록 안전장치가 추가된 버전) -/// -/// 핵심 흐름: -/// 1. PlayerBehaviorTracker에서 실시간 행동 데이터를 읽음 -/// 2. 임계치 판단 → 카운터 모드 ON/OFF (잠금 해제 여부에 따라 임계치 다름) -/// 3. 가중치 기반으로 보스 패턴 선택 -/// 4. 첫 발동 시 해당 카운터를 영구 잠금 해제 -/// 5. 플레이어가 습관을 바꾸면 보상 (난이도 완화) /// -public class BossCounterSystem : MonoBehaviour +public class BossCounterSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterSystem을 { - [Header("참조")] - [SerializeField] private BossCounterConfig config; + [Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를 + [SerializeField] private BossCounterConfig config; // 변수를 선언할거에요 -> 카운터 설정 파일(BossCounterConfig)을 담을 config를 - [Header("이벤트 (연출/UI 연동)")] - [Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")] - public UnityEvent OnCounterActivated; + [Header("이벤트 (연출/UI 연동)")] // 인스펙터 창에 제목을 표시할거에요 -> 이벤트 (연출/UI 연동) 을 + [Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public UnityEvent OnCounterActivated; // 이벤트를 선언할거에요 -> 카운터가 켜질 때 알릴 OnCounterActivated를 - [Tooltip("카운터 모드가 비활성화될 때 발생")] - public UnityEvent OnCounterDeactivated; + [Tooltip("카운터 모드가 비활성화될 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public UnityEvent OnCounterDeactivated; // 이벤트를 선언할거에요 -> 카운터가 꺼질 때 알릴 OnCounterDeactivated를 - [Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")] - public UnityEvent OnCounterPatternSelected; + [Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public UnityEvent OnCounterPatternSelected; // 이벤트를 선언할거에요 -> 패턴이 선택됐을 때 알릴 OnCounterPatternSelected를 - [Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")] - public UnityEvent OnHabitChangeRewarded; + [Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public UnityEvent OnHabitChangeRewarded; // 이벤트를 선언할거에요 -> 습관 변경 보상을 알릴 OnHabitChangeRewarded를 // ── 카운터 모드 상태 ── - private Dictionary activeCounters = new Dictionary() + private Dictionary activeCounters = new Dictionary() // 변수를 선언하고 초기화할거에요 -> 각 카운터 타입의 활성 상태를 저장할 딕셔너리 activeCounters를 { - { CounterType.Dodge, false }, - { CounterType.Aim, false }, - { CounterType.Pierce, false } + { CounterType.Dodge, false }, // 값을 넣을거에요 -> 회피 카운터 초기값을 거짓(false)으로 + { CounterType.Aim, false }, // 값을 넣을거에요 -> 조준 카운터 초기값을 거짓(false)으로 + { CounterType.Pierce, false } // 값을 넣을거에요 -> 관통 카운터 초기값을 거짓(false)으로 }; // ── Decay 타이머 ── - private Dictionary counterTimers = new Dictionary() + private Dictionary counterTimers = new Dictionary() // 변수를 선언하고 초기화할거에요 -> 각 카운터의 남은 지속 시간을 저장할 딕셔너리 counterTimers를 { - { CounterType.Dodge, 0f }, - { CounterType.Aim, 0f }, - { CounterType.Pierce, 0f } + { CounterType.Dodge, 0f }, // 값을 넣을거에요 -> 회피 카운터 타이머를 0으로 + { CounterType.Aim, 0f }, // 값을 넣을거에요 -> 조준 카운터 타이머를 0으로 + { CounterType.Pierce, 0f } // 값을 넣을거에요 -> 관통 카운터 타이머를 0으로 }; // ── 쿨타임 ── - private float lastCounterPatternTime = -100f; + private float lastCounterPatternTime = -100f; // 변수를 선언할거에요 -> 마지막 카운터 패턴 사용 시간을 lastCounterPatternTime에 (초기값 -100) // ── 습관 변경 보상 추적 ── - private HashSet previousRunCounters = new HashSet(); - private HashSet currentRunActivatedCounters = new HashSet(); - private bool habitChangeRewardGranted = false; + private HashSet previousRunCounters = new HashSet(); // 변수를 선언할거에요 -> 지난 런에서 당했던 카운터 목록을 previousRunCounters에 + private HashSet currentRunActivatedCounters = new HashSet(); // 변수를 선언할거에요 -> 이번 런에서 발동된 카운터 목록을 currentRunActivatedCounters에 + private bool habitChangeRewardGranted = false; // 변수를 선언할거에요 -> 보상이 이미 지급되었는지 여부를 habitChangeRewardGranted에 // ── 보스 패턴 가중치 정의 ── - [System.Serializable] - public class BossPattern + [System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정할 수 있게 BossPattern 클래스를 + public class BossPattern // 내부 클래스를 선언할거에요 -> 보스 패턴 정보를 담을 BossPattern을 { - public string patternName; - public float baseWeight = 1f; - [Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")] - public CounterType counterType = CounterType.None; - [Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")] - public CounterType subCounterType = CounterType.None; + public string patternName; // 변수를 선언할거에요 -> 패턴 이름을 저장할 patternName을 + public float baseWeight = 1f; // 변수를 선언할거에요 -> 기본 가중치(확률)를 baseWeight에 + [Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public CounterType counterType = CounterType.None; // 변수를 선언할거에요 -> 이 패턴이 어떤 카운터 타입인지 지정할 counterType을 + [Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public CounterType subCounterType = CounterType.None; // 변수를 선언할거에요 -> 보너스 가중치를 받을 보조 카운터 타입 subCounterType을 } - [Header("보스 패턴 목록")] - [SerializeField] private List bossPatterns = new List(); + [Header("보스 패턴 목록")] // 인스펙터 창에 제목을 표시할거에요 -> 보스 패턴 목록 을 + [SerializeField] private List bossPatterns = new List(); // 리스트를 선언할거에요 -> 보스의 모든 패턴 정보를 담을 bossPatterns를 // ═══════════════════════════════════════════ // 초기화 // ═══════════════════════════════════════════ - private void Start() + private void Start() // 함수를 실행할거에요 -> 게임 시작 시 호출되는 Start를 { // 🚨 [안전장치] 설정 파일이 없으면 경고 출력 - if (config == null) + if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일(config)이 연결되지 않았다면 { - Debug.LogWarning("⚠ [BossCounterSystem] Config(설정 파일)가 없습니다! AI가 정상 작동하지 않을 수 있습니다."); + Debug.LogWarning("⚠ [BossCounterSystem] Config(설정 파일)가 없습니다! AI가 정상 작동하지 않을 수 있습니다."); // 경고 로그를 출력할거에요 -> 설정 파일 누락 경고를 } } /// /// 보스 전투 시작 시 호출. 이전 런에서 발동된 카운터 정보를 기반으로 초기화. /// - public void InitializeBattle() + public void InitializeBattle() // 함수를 선언할거에요 -> 전투 시작 시 초기화를 담당할 InitializeBattle을 { // 이전 런에서 잠금 해제된 카운터들을 기록 (습관 변경 보상 판단용) - previousRunCounters.Clear(); - currentRunActivatedCounters.Clear(); - habitChangeRewardGranted = false; + previousRunCounters.Clear(); // 비울거에요 -> 이전 런 카운터 목록을 + currentRunActivatedCounters.Clear(); // 비울거에요 -> 이번 런 카운터 목록을 + habitChangeRewardGranted = false; // 초기화할거에요 -> 보상 지급 여부를 거짓(false)으로 - var persistence = BossCounterPersistence.Instance; - if (persistence != null) + var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에 + if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 존재한다면 { - if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge)) - previousRunCounters.Add(CounterType.Dodge); - if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim)) - previousRunCounters.Add(CounterType.Aim); - if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce)) - previousRunCounters.Add(CounterType.Pierce); + if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge)) // 조건이 맞으면 실행할거에요 -> 회피 카운터 기록이 있고 해제되었다면 + previousRunCounters.Add(CounterType.Dodge); // 추가할거에요 -> 회피 타입을 이전 런 목록에 + if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim)) // 조건이 맞으면 실행할거에요 -> 조준 카운터 기록이 있고 해제되었다면 + previousRunCounters.Add(CounterType.Aim); // 추가할거에요 -> 조준 타입을 이전 런 목록에 + if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce)) // 조건이 맞으면 실행할거에요 -> 관통 카운터 기록이 있고 해제되었다면 + previousRunCounters.Add(CounterType.Pierce); // 추가할거에요 -> 관통 타입을 이전 런 목록에 } // 모든 카운터 모드 OFF - foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) + foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) // 반복할거에요 -> 모든 카운터 타입(회피, 조준, 관통)에 대해 { - activeCounters[type] = false; - counterTimers[type] = 0f; + activeCounters[type] = false; // 상태를 바꿀거에요 -> 해당 카운터를 비활성화(false)로 + counterTimers[type] = 0f; // 값을 바꿀거에요 -> 해당 카운터 타이머를 0으로 } - lastCounterPatternTime = -100f; + lastCounterPatternTime = -100f; // 값을 초기화할거에요 -> 마지막 패턴 사용 시간을 -100으로 - Debug.Log($"[BossCounter] 전투 시작. 이전 런 카운터: [{string.Join(", ", previousRunCounters)}]"); + Debug.Log($"[BossCounter] 전투 시작. 이전 런 카운터: [{string.Join(", ", previousRunCounters)}]"); // 로그를 출력할거에요 -> 전투 시작 알림과 이전 런 정보를 } // ═══════════════════════════════════════════ // 매 프레임 업데이트 // ═══════════════════════════════════════════ - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 { // 🚨 [안전장치] 플레이어 트래커나 설정 파일이 없으면 실행 중지 (NullReferenceException 방지) - if (PlayerBehaviorTracker.Instance == null || config == null) return; + if (PlayerBehaviorTracker.Instance == null || config == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 추적기나 설정 파일이 없다면 - EvaluateCounters(); - DecayCounters(); - CheckHabitChangeReward(); + EvaluateCounters(); // 함수를 실행할거에요 -> 카운터 발동 조건을 검사하는 EvaluateCounters를 + DecayCounters(); // 함수를 실행할거에요 -> 카운터 지속 시간을 관리하는 DecayCounters를 + CheckHabitChangeReward(); // 함수를 실행할거에요 -> 습관 변경 보상을 체크하는 CheckHabitChangeReward를 } /// 플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단 - private void EvaluateCounters() + private void EvaluateCounters() // 함수를 선언할거에요 -> 카운터 발동 여부를 판단하는 EvaluateCounters를 { // 🚨 [안전장치] Config가 없으면 계산 불가 - if (config == null) return; + if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면 - var tracker = PlayerBehaviorTracker.Instance; - var persistence = BossCounterPersistence.Instance; + var tracker = PlayerBehaviorTracker.Instance; // 변수를 가져올거에요 -> 플레이어 행동 추적기를 tracker에 + var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에 // ── 회피 카운터 ── - bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge); - int dodgeThreshold = config.GetEffectiveDodgeThreshold(dodgeUnlocked); + bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge); // 값을 확인할거에요 -> 회피 카운터가 해금되었는지 여부를 + int dodgeThreshold = config.GetEffectiveDodgeThreshold(dodgeUnlocked); // 값을 가져올거에요 -> 현재 적용할 회피 임계값을 - if (tracker.DodgeCount >= dodgeThreshold) + if (tracker.DodgeCount >= dodgeThreshold) // 조건이 맞으면 실행할거에요 -> 플레이어 회피 횟수가 임계값 이상이라면 { // 잠금 해제 안 된 상태면 첫 발동 시 잠금 해제 (첫 런에서도 발동 가능) // 잠금 해제된 상태면 더 낮은 임계치로 발동 - ActivateCounter(CounterType.Dodge); + ActivateCounter(CounterType.Dodge); // 함수를 실행할거에요 -> 회피 카운터를 발동시키는 ActivateCounter를 } // ── 조준 카운터 ── - bool aimUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Aim); - float aimThreshold = config.GetEffectiveAimThreshold(aimUnlocked); + bool aimUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Aim); // 값을 확인할거에요 -> 조준 카운터가 해금되었는지 여부를 + float aimThreshold = config.GetEffectiveAimThreshold(aimUnlocked); // 값을 가져올거에요 -> 현재 적용할 조준 임계값을 - if (tracker.AimHoldTime >= aimThreshold) + if (tracker.AimHoldTime >= aimThreshold) // 조건이 맞으면 실행할거에요 -> 플레이어 조준 시간이 임계값 이상이라면 { - ActivateCounter(CounterType.Aim); + ActivateCounter(CounterType.Aim); // 함수를 실행할거에요 -> 조준 카운터를 발동시키는 ActivateCounter를 } // ── 관통 카운터 ── - bool pierceUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Pierce); - float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked); + bool pierceUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Pierce); // 값을 확인할거에요 -> 관통 카운터가 해금되었는지 여부를 + float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked); // 값을 가져올거에요 -> 현재 적용할 관통 임계값을 - if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck - && tracker.PierceRatio >= pierceThreshold) + if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck // 조건이 맞으면 실행할거에요 -> 최소 발사 횟수를 충족하고 + && tracker.PierceRatio >= pierceThreshold) // 조건이 맞으면 실행할거에요 -> 관통 공격 비율이 임계값 이상이라면 { - ActivateCounter(CounterType.Pierce); + ActivateCounter(CounterType.Pierce); // 함수를 실행할거에요 -> 관통 카운터를 발동시키는 ActivateCounter를 } } - private void ActivateCounter(CounterType type) + private void ActivateCounter(CounterType type) // 함수를 선언할거에요 -> 특정 카운터를 활성화하는 ActivateCounter를 { // 🚨 [안전장치] Config 확인 - if (config == null) return; + if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면 - counterTimers[type] = config.counterDecayTime; + counterTimers[type] = config.counterDecayTime; // 값을 설정할거에요 -> 해당 카운터의 지속 시간을 설정값으로 - if (!activeCounters[type]) + if (!activeCounters[type]) // 조건이 맞으면 실행할거에요 -> 해당 카운터가 현재 꺼져 있다면 { - activeCounters[type] = true; - currentRunActivatedCounters.Add(type); + activeCounters[type] = true; // 상태를 바꿀거에요 -> 카운터 활성 상태를 참(true)으로 + currentRunActivatedCounters.Add(type); // 추가할거에요 -> 이번 런 발동 목록에 해당 타입을 // 영구 잠금 해제 - var persistence = BossCounterPersistence.Instance; - if (persistence != null) + var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에 + if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면 { - persistence.UnlockCounter(type); - persistence.RecordActivation(type); + persistence.UnlockCounter(type); // 함수를 실행할거에요 -> 해당 카운터를 영구 해금하는 UnlockCounter를 + persistence.RecordActivation(type); // 함수를 실행할거에요 -> 발동 횟수를 기록하는 RecordActivation을 } - OnCounterActivated?.Invoke(type); - Debug.Log($"[BossCounter] {type} 카운터 활성화!"); + OnCounterActivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 활성화 알림 이벤트를 + Debug.Log($"[BossCounter] {type} 카운터 활성화!"); // 로그를 출력할거에요 -> 카운터 활성화 메시지를 } } /// 시간이 지나면 카운터 모드 자동 해제 - private void DecayCounters() + private void DecayCounters() // 함수를 선언할거에요 -> 시간이 지나면 카운터를 끄는 DecayCounters를 { - foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) + foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) // 반복할거에요 -> 모든 카운터 타입에 대해 { - if (!activeCounters[type]) continue; + if (!activeCounters[type]) continue; // 조건이 맞으면 건너뛸거에요 -> 이미 꺼져 있는 카운터라면 - counterTimers[type] -= Time.deltaTime; - if (counterTimers[type] <= 0f) + counterTimers[type] -= Time.deltaTime; // 값을 뺄거에요 -> 남은 시간에서 프레임 시간을 + if (counterTimers[type] <= 0f) // 조건이 맞으면 실행할거에요 -> 남은 시간이 0 이하라면 { - activeCounters[type] = false; - OnCounterDeactivated?.Invoke(type); - Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화"); + activeCounters[type] = false; // 상태를 바꿀거에요 -> 카운터 활성 상태를 거짓(false)으로 + OnCounterDeactivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 비활성화 알림 이벤트를 + Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화"); // 로그를 출력할거에요 -> 시간 만료로 인한 비활성화 메시지를 } } } @@ -220,25 +213,25 @@ public class BossCounterSystem : MonoBehaviour /// 이전 런에서 카운터 당한 습관을 이번 런에서 보이지 않으면 보상. /// 전투 중반(30초 이후) 한 번 체크. /// - private float battleTimer = 0f; - private const float HABIT_CHECK_TIME = 30f; + private float battleTimer = 0f; // 변수를 초기화할거에요 -> 전투 진행 시간을 0으로 + private const float HABIT_CHECK_TIME = 30f; // 상수를 정의할거에요 -> 습관 체크 시점을 30초로 - private void CheckHabitChangeReward() + private void CheckHabitChangeReward() // 함수를 선언할거에요 -> 습관 변경 보상을 확인하는 CheckHabitChangeReward를 { - if (habitChangeRewardGranted || previousRunCounters.Count == 0) return; + if (habitChangeRewardGranted || previousRunCounters.Count == 0) return; // 조건이 맞으면 중단할거에요 -> 이미 보상을 받았거나 이전 런 기록이 없다면 - battleTimer += Time.deltaTime; - if (battleTimer < HABIT_CHECK_TIME) return; + battleTimer += Time.deltaTime; // 값을 더할거에요 -> 전투 진행 시간에 프레임 시간을 + if (battleTimer < HABIT_CHECK_TIME) return; // 조건이 맞으면 중단할거에요 -> 아직 30초가 안 지났다면 // 이전 런에서 발동된 카운터 중, 이번 런에서 아직 발동 안 된 것이 있으면 보상 - foreach (var prevCounter in previousRunCounters) + foreach (var prevCounter in previousRunCounters) // 반복할거에요 -> 이전 런 카운터 목록에 대해 { - if (!currentRunActivatedCounters.Contains(prevCounter)) + if (!currentRunActivatedCounters.Contains(prevCounter)) // 조건이 맞으면 실행할거에요 -> 이번 런 목록에 해당 카운터가 없다면 (습관 고침) { - habitChangeRewardGranted = true; - OnHabitChangeRewarded?.Invoke(); - Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여"); - break; + habitChangeRewardGranted = true; // 상태를 바꿀거에요 -> 보상 지급 여부를 참(true)으로 + OnHabitChangeRewarded?.Invoke(); // 이벤트를 실행할거에요 -> 보상 지급 알림 이벤트를 + Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여"); // 로그를 출력할거에요 -> 보상 부여 메시지를 + break; // 중단할거에요 -> 보상은 한 번만 주므로 반복문을 } } } @@ -252,106 +245,106 @@ public class BossCounterSystem : MonoBehaviour /// 보스 AI의 패턴 선택 시점에 호출하세요. /// /// 선택된 패턴 이름 - public string SelectBossPattern() + public string SelectBossPattern() // 함수를 선언할거에요 -> 보스 패턴을 선택해서 반환하는 SelectBossPattern을 { - if (bossPatterns.Count == 0) + if (bossPatterns.Count == 0) // 조건이 맞으면 실행할거에요 -> 등록된 패턴이 하나도 없다면 { - Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!"); - return "Normal"; // 기본값 리턴 + Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!"); // 경고 로그를 출력할거에요 -> 패턴 없음 경고를 + return "Normal"; // 값을 반환할거에요 -> 기본값 "Normal"을 } // 🚨 [안전장치] Config가 없으면 그냥 랜덤 패턴 반환 (멈춤 방지) - if (config == null) + if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일이 없다면 { - int randomIndex = Random.Range(0, bossPatterns.Count); - return bossPatterns[randomIndex].patternName; + int randomIndex = Random.Range(0, bossPatterns.Count); // 값을 랜덤으로 뽑을거에요 -> 0부터 패턴 개수 사이의 인덱스를 + return bossPatterns[randomIndex].patternName; // 값을 반환할거에요 -> 랜덤하게 뽑힌 패턴 이름을 } // 가중치 계산 - float[] weights = new float[bossPatterns.Count]; - float totalWeight = 0f; - float counterWeight = 0f; + float[] weights = new float[bossPatterns.Count]; // 배열을 만들거에요 -> 각 패턴의 가중치를 담을 weights 배열을 + float totalWeight = 0f; // 변수를 초기화할거에요 -> 전체 가중치 합계를 0으로 + float counterWeight = 0f; // 변수를 초기화할거에요 -> 카운터 패턴들의 가중치 합계를 0으로 - for (int i = 0; i < bossPatterns.Count; i++) + for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해 { - weights[i] = bossPatterns[i].baseWeight; + weights[i] = bossPatterns[i].baseWeight; // 값을 넣을거에요 -> 기본 가중치를 배열에 // 카운터 패턴 가중치 증가 - if (bossPatterns[i].counterType != CounterType.None - && activeCounters.ContainsKey(bossPatterns[i].counterType) - && activeCounters[bossPatterns[i].counterType]) + if (bossPatterns[i].counterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 카운터 타입이 설정되어 있고 + && activeCounters.ContainsKey(bossPatterns[i].counterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며 + && activeCounters[bossPatterns[i].counterType]) // 조건이 맞으면 실행할거에요 -> 해당 카운터가 활성 상태라면 { // 쿨타임 체크 - if (Time.time - lastCounterPatternTime >= config.counterCooldown) + if (Time.time - lastCounterPatternTime >= config.counterCooldown) // 조건이 맞으면 실행할거에요 -> 마지막 사용 후 쿨타임이 지났다면 { - weights[i] += config.counterWeightBonus; + weights[i] += config.counterWeightBonus; // 값을 더할거에요 -> 카운터 보너스 가중치를 } } // 보조 카운터 가중치 - if (bossPatterns[i].subCounterType != CounterType.None - && activeCounters.ContainsKey(bossPatterns[i].subCounterType) - && activeCounters[bossPatterns[i].subCounterType]) + if (bossPatterns[i].subCounterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 보조 카운터 타입이 설정되어 있고 + && activeCounters.ContainsKey(bossPatterns[i].subCounterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며 + && activeCounters[bossPatterns[i].subCounterType]) // 조건이 맞으면 실행할거에요 -> 해당 보조 카운터가 활성 상태라면 { - weights[i] += config.counterSubWeightBonus; + weights[i] += config.counterSubWeightBonus; // 값을 더할거에요 -> 보조 카운터 보너스 가중치를 } // 습관 변경 보상: 일반 패턴 가중치 감소 (= 난이도 완화) - if (habitChangeRewardGranted && bossPatterns[i].counterType == CounterType.None) + if (habitChangeRewardGranted && bossPatterns[i].counterType == CounterType.None) // 조건이 맞으면 실행할거에요 -> 보상을 받았고 일반 패턴이라면 { - weights[i] *= (1f - config.habitChangeRewardRatio); + weights[i] *= (1f - config.habitChangeRewardRatio); // 값을 곱할거에요 -> 가중치를 감소 비율만큼 줄여서 } - totalWeight += weights[i]; + totalWeight += weights[i]; // 값을 더할거에요 -> 전체 가중치 합계에 현재 가중치를 - if (bossPatterns[i].counterType != CounterType.None) - counterWeight += weights[i]; + if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면 + counterWeight += weights[i]; // 값을 더할거에요 -> 카운터 가중치 합계에 } // 빈도 상한 체크: 카운터 패턴 총 비율이 maxCounterFrequency를 넘지 않도록 - if (totalWeight > 0f && counterWeight / totalWeight > config.maxCounterFrequency) + if (totalWeight > 0f && counterWeight / totalWeight > config.maxCounterFrequency) // 조건이 맞으면 실행할거에요 -> 카운터 패턴 비율이 최대 허용치를 초과한다면 { - float allowedCounterWeight = (totalWeight - counterWeight) * config.maxCounterFrequency / (1f - config.maxCounterFrequency); - float scale = allowedCounterWeight / counterWeight; + float allowedCounterWeight = (totalWeight - counterWeight) * config.maxCounterFrequency / (1f - config.maxCounterFrequency); // 값을 계산할거에요 -> 허용 가능한 카운터 총 가중치를 + float scale = allowedCounterWeight / counterWeight; // 값을 계산할거에요 -> 가중치를 줄일 비율(scale)을 - for (int i = 0; i < bossPatterns.Count; i++) + for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해 { - if (bossPatterns[i].counterType != CounterType.None) + if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면 { - float bonus = weights[i] - bossPatterns[i].baseWeight; - weights[i] = bossPatterns[i].baseWeight + bonus * scale; + float bonus = weights[i] - bossPatterns[i].baseWeight; // 값을 계산할거에요 -> 추가된 보너스 가중치를 + weights[i] = bossPatterns[i].baseWeight + bonus * scale; // 값을 수정할거에요 -> 보너스를 비율대로 줄여서 다시 적용 } } // 총 가중치 재계산 - totalWeight = 0f; - for (int i = 0; i < weights.Length; i++) - totalWeight += weights[i]; + totalWeight = 0f; // 값을 초기화할거에요 -> 전체 합계를 0으로 + for (int i = 0; i < weights.Length; i++) // 반복할거에요 -> 모든 가중치에 대해 + totalWeight += weights[i]; // 값을 더할거에요 -> 전체 합계에 } // 가중치 랜덤 선택 - float roll = Random.Range(0f, totalWeight); - float cumulative = 0f; + float roll = Random.Range(0f, totalWeight); // 값을 랜덤으로 뽑을거에요 -> 0부터 전체 가중치 사이의 값을 roll에 + float cumulative = 0f; // 변수를 초기화할거에요 -> 누적 가중치를 0으로 - for (int i = 0; i < bossPatterns.Count; i++) + for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해 { - cumulative += weights[i]; - if (roll <= cumulative) + cumulative += weights[i]; // 값을 더할거에요 -> 누적 가중치에 현재 가중치를 + if (roll <= cumulative) // 조건이 맞으면 실행할거에요 -> 랜덤값이 누적치보다 작거나 같다면 (당첨) { - string selected = bossPatterns[i].patternName; + string selected = bossPatterns[i].patternName; // 값을 저장할거에요 -> 선택된 패턴 이름을 selected에 // 카운터 패턴이면 쿨타임 기록 - if (bossPatterns[i].counterType != CounterType.None) + if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면 { - lastCounterPatternTime = Time.time; + lastCounterPatternTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 사용 시간으로 } - OnCounterPatternSelected?.Invoke(selected); - return selected; + OnCounterPatternSelected?.Invoke(selected); // 이벤트를 실행할거에요 -> 패턴 선택 알림 이벤트를 + return selected; // 값을 반환할거에요 -> 선택된 패턴 이름을 } } - return bossPatterns[bossPatterns.Count - 1].patternName; + return bossPatterns[bossPatterns.Count - 1].patternName; // 값을 반환할거에요 -> 만약 선택되지 않았다면 마지막 패턴을 (안전장치) } // ═══════════════════════════════════════════ @@ -359,22 +352,22 @@ public class BossCounterSystem : MonoBehaviour // ═══════════════════════════════════════════ /// 특정 카운터 모드가 현재 활성 상태인지 확인 - public bool IsCounterActive(CounterType type) + public bool IsCounterActive(CounterType type) // 함수를 선언할거에요 -> 특정 카운터 활성 여부를 반환하는 IsCounterActive를 { - return activeCounters.ContainsKey(type) && activeCounters[type]; + return activeCounters.ContainsKey(type) && activeCounters[type]; // 값을 반환할거에요 -> 딕셔너리에 키가 있고 값이 참(true)인지를 } /// 현재 활성화된 모든 카운터 타입 반환 - public List GetActiveCounters() + public List GetActiveCounters() // 함수를 선언할거에요 -> 활성화된 모든 카운터 목록을 반환하는 GetActiveCounters를 { - var result = new List(); - foreach (var kvp in activeCounters) + var result = new List(); // 리스트를 만들거에요 -> 결과를 담을 result 리스트를 + foreach (var kvp in activeCounters) // 반복할거에요 -> 모든 카운터 상태에 대해 { - if (kvp.Value) result.Add(kvp.Key); + if (kvp.Value) result.Add(kvp.Key); // 조건이 맞으면 실행할거에요 -> 활성화(true) 상태라면 리스트에 추가하기를 } - return result; + return result; // 값을 반환할거에요 -> 완성된 리스트를 } /// 습관 변경 보상이 활성화되었는지 확인 - public bool IsHabitChangeRewarded => habitChangeRewardGranted; + public bool IsHabitChangeRewarded => habitChangeRewardGranted; // 프로퍼티를 선언할거에요 -> 보상 지급 여부를 외부에서 읽을 수 있는 IsHabitChangeRewarded를 } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/BossMonster.cs b/Assets/Scripts/Enemy/BossAI/BossMonster.cs index b92364de..37db27b7 100644 --- a/Assets/Scripts/Enemy/BossAI/BossMonster.cs +++ b/Assets/Scripts/Enemy/BossAI/BossMonster.cs @@ -1,460 +1,443 @@ -using UnityEngine; -using System.Collections; +using UnityEngine; // 유니티 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 -public class NorcielBoss : MonsterClass +public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를 { - [Header("--- 🧠 두뇌 연결 ---")] - [SerializeField] private BossCounterSystem counterSystem; + [Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🧠 두뇌 연결 ---을 + [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 보스의 패턴을 결정할 counterSystem을 - [Header("--- ⚔️ 패턴 설정 ---")] - [SerializeField] private float patternInterval = 3f; + [Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- ⚔️ 패턴 설정 ---을 + [SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 대기 시간을 3초로 지정하는 patternInterval을 + [SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 이 거리 안에 플레이어가 들어오면 공격하는 attackRange를 3으로 - [Header("--- 🎱 무기(쇠공) 설정 (필수!) ---")] - [SerializeField] private GameObject ironBall; - [SerializeField] private Transform handHolder; + [Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🎱 무기 설정 ---을 + [SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 던지고 주울 무기 오브젝트인 ironBall을 + [SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 무기를 잡을 손의 위치인 handHolder를 - [Header("--- 📊 UI 연결 ---")] - [SerializeField] private GameObject bossHealthBar; + [Header("--- 📊 UI 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 📊 UI 연결 ---을 + [SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 보스의 체력 UI를 담을 bossHealthBar를 + + // ⭐ [핵심 수정] 애니메이션 이름을 인스펙터에서 맞출 수 있게 변수로 분리 + [Header("--- 🎬 애니메이션 이름 설정 (Animator와 일치시킬 것!) ---")] + [SerializeField] private string anim_Roar = "Roar"; // 변수를 선언할거에요 -> 포효 애니메이션 이름을 + [SerializeField] private string anim_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름을 + [SerializeField] private string anim_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 + [SerializeField] private string anim_Pickup = "Skill_Pickup"; // 변수를 선언할거에요 -> 무기 줍기 애니메이션 이름을 + [SerializeField] private string anim_Throw = "Attack_Throw"; // 변수를 선언할거에요 -> 던지기 애니메이션 이름을 + + [Header("--- 🎬 스킬 애니메이션 이름 ---")] + [SerializeField] private string anim_DashReady = "Skill_Dash_Ready"; // 변수를 선언할거에요 -> 돌진 준비 애니메이션 이름을 + [SerializeField] private string anim_DashGo = "Skill_Dash_Go"; // 변수를 선언할거에요 -> 돌진 출발 애니메이션 이름을 + [SerializeField] private string anim_SmashCharge = "Skill_Smash_Charge"; // 변수를 선언할거에요 -> 내려찍기 기모으기 이름을 + [SerializeField] private string anim_SmashImpact = "Skill_Smash_Impact"; // 변수를 선언할거에요 -> 내려찍기 타격 이름을 + [SerializeField] private string anim_Sweep = "Skill_Sweep"; // 변수를 선언할거에요 -> 휩쓸기 애니메이션 이름을 + + [Header("--- 🔍 디버그 (읽기 전용) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🔍 디버그 ---를 + [SerializeField] private string debugState = "대기 중"; // 변수를 선언할거에요 -> 현재 보스 행동을 문장으로 보여줄 debugState를 // --- 내부 변수들 --- - private float _timer; - private Rigidbody rb; - private Rigidbody ballRb; + private float _timer; // 변수를 선언할거에요 -> 공격 쿨타임을 계산할 내부 타이머 _timer를 + private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 엔진을 제어할 rb를 + private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 엔진을 제어할 ballRb를 - private bool isBattleStarted = false; - private bool isWeaponless = false; - private bool isPerformingAction = false; // ⭐ 신규: 연출 중 플래그 (Roar 등) - private Transform target; + private bool isBattleStarted = false; // 변수를 초기화할거에요 -> 전투 시작 상태를 거짓(false)으로 + private bool isWeaponless = false; // 변수를 초기화할거에요 -> 맨손 상태를 거짓(false)으로 + private bool isPerformingAction = false; // 변수를 초기화할거에요 -> 연출 진행 상태를 거짓(false)으로 + private Transform target; // 변수를 선언할거에요 -> 보스가 쫓아갈 대상의 위치인 target을 - protected override void Awake() + protected override void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를 { - base.Awake(); - rb = GetComponent(); - if (ironBall != null) ballRb = ironBall.GetComponent(); + base.Awake(); // 부모 클래스의 함수를 먼저 실행할거에요 -> MonsterClass의 Awake 로직을 + rb = GetComponent(); // 컴포넌트를 가져와서 저장할거에요 -> 보스 자신의 Rigidbody를 rb에 + if (ironBall != null) ballRb = ironBall.GetComponent(); // 조건이 맞으면 가져올거에요 -> ironBall이 있다면 그것의 Rigidbody를 ballRb에 } - // ⭐ [수정] Start → OnEnable 이후 호출되도록 변경 - private void Start() + private void Start() // 함수를 실행할거에요 -> 첫 프레임이 시작될 때 호출되는 Start를 { - StartBossBattle(); + StartBossBattle(); // 함수를 실행할거에요 -> Roar 연출 후 전투를 시작하는 StartBossBattle을 } - protected override void Init() + protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 몬스터 초기화를 담당하는 Init을 { - _timer = patternInterval; - isBattleStarted = false; - isWeaponless = false; - isPerformingAction = false; + base.Init(); // 부모 초기화 호출 + _timer = 0f; // 값을 넣을거에요 -> 전투 시작하자마자 첫 공격이 가능하게 타이머를 0으로 + isBattleStarted = false; // 상태를 바꿀거에요 -> 전투 시작 상태를 거짓(false)으로 + isWeaponless = false; // 상태를 바꿀거에요 -> 맨손 상태를 거짓(false)으로 + isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 상태를 거짓(false)으로 - // 플레이어 찾기 - GameObject playerObj = GameObject.FindWithTag("Player"); - if (playerObj != null) target = playerObj.transform; - else + IsAggroed = true; // 상태를 바꿀거에요 -> 부모 클래스의 영구 어그로 상태를 참(true)으로 + mobRenderer = null; // 값을 지울거에요 -> 화면 밖 최적화를 끄기 위해 렌더러를 null로 + optimizationDistance = 9999f; // 값을 바꿀거에요 -> 거리가 멀어져도 멈추지 않게 최적화 거리를 9999로 + + GameObject playerObj = GameObject.FindWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그가 붙은 녀석을 + if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 태그로 찾은 플레이어가 존재한다면 { - var playerScript = FindObjectOfType(); - if (playerScript != null) target = playerScript.transform; + target = playerObj.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에 + playerTransform = playerObj.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를 + } + else // 조건이 틀리면 실행할거에요 -> 태그로 찾지 못했다면 + { + var playerScript = FindObjectOfType(); // 컴포넌트를 찾을거에요 -> PlayerMovement 스크립트를 가진 오브젝트를 + if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 스크립트로 플레이어를 찾았다면 + { + target = playerScript.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에 + playerTransform = playerScript.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를 + } } - // HP 초기화 - currentHP = maxHP; + if (target == null) // 조건이 맞으면 실행할거에요 -> 어떤 방법으로도 플레이어를 못 찾았다면 + Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 출력할거에요 -> 플레이어 없음 경고 메시지를 - StartCoroutine(SafeInitNavMesh()); + currentHP = maxHP; // 체력을 채울거에요 -> 현재 체력을 최대 체력으로 + StartCoroutine(SafeInitNavMesh()); // 코루틴을 실행할거에요 -> 길찾기를 안전하게 켜는 SafeInitNavMesh를 - if (bossHealthBar != null) bossHealthBar.SetActive(false); - if (ballRb != null) ballRb.isKinematic = true; + if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를 + if (ballRb != null) ballRb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 쇠공 물리가 있다면 중력 연산을 끄기를 + if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 대기 모션을 } - private IEnumerator SafeInitNavMesh() + private IEnumerator SafeInitNavMesh() // 코루틴 함수를 선언할거에요 -> 길찾기를 안전하게 초기화하는 SafeInitNavMesh를 { - yield return null; - - if (agent != null) + yield return null; // 기다릴거에요 -> 다음 프레임이 될 때까지 한 턴을 + if (agent != null) // 조건이 맞으면 실행할거에요 -> NavMeshAgent가 존재한다면 { - int retry = 0; - while (!agent.isOnNavMesh && retry < 5) + int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로 + while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 바닥에 없고 시도가 5번 미만인 동안 { - retry++; - yield return new WaitForSeconds(0.1f); + retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼 + yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을 } + if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 바닥에 닿았다면 이동을 정지로 + agent.enabled = false; // 기능을 끌거에요 -> 전투 시작 전까지 NavMeshAgent를 비활성화로 + } + } - if (agent.isOnNavMesh) - { - agent.isStopped = true; - } - agent.enabled = false; + protected override void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 + { + base.Update(); // 부모 클래스의 Update도 호출 (최적화 로직 등) + + if (!isDead) // 조건이 맞으면 실행할거에요 -> 보스가 살아있다면 + { + ExecuteAILogic(); // 함수를 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을 } } // ════════════════════════════════════════ - // ⭐ [핵심 수정] 부모의 OnManagedUpdate 흐름 제어 + // 🧠 AI 핵심 로직 // ════════════════════════════════════════ - /// - /// 부모 MonsterClass의 StopMovement()가 매 프레임 Monster_Idle을 - /// 강제 재생해서 Roar/다른 애니메이션을 덮어쓰는 문제 해결. - /// - /// 보스는 일반 몬스터와 다르게: - /// - 전투 시작 전에는 아무 AI도 돌리지 않음 - /// - 연출 중(Roar 등)에는 애니메이션을 건드리지 않음 - /// - protected override void ExecuteAILogic() + protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을 { - // 연출 중이거나 전투 미시작 → 아무것도 안 함 - if (isPerformingAction || !isBattleStarted || target == null) return; + if (isPerformingAction || !isBattleStarted || target == null) return; // 조건이 맞으면 중단할거에요 -> 연출중, 전투전, 타겟없음 중 하나라도 해당되면 - // 무기 없으면 회수 우선 - if (isWeaponless) + if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 쇠공을 던져서 맨손이라면 { - RetrieveWeaponLogic(); - return; + RetrieveWeaponLogic(); // 함수를 실행할거에요 -> 쇠공을 주우러 가는 로직을 + return; // 중단할거에요 -> 무기 줍기가 우선이므로 아래 로직을 } - // --- 일반 전투 로직 --- - _timer -= Time.deltaTime; - if (_timer <= 0 && !isAttacking && !isHit && !isDead && !isResting) + if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 AI 판단을 + + if (_timer > 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 남아있다면 { - _timer = patternInterval; - DecideAttack(); + _timer -= Time.deltaTime; // 값을 뺄거에요 -> 쿨타임 타이머에서 지난 프레임 시간만큼을 } - // 이동 (공격/피격 중이 아닐 때만) - if (!isAttacking && !isHit && !isResting && agent != null && agent.enabled && agent.isOnNavMesh) + float distToPlayer = Vector3.Distance(transform.position, target.position); // 거리를 계산할거에요 -> 보스와 플레이어 사이의 거리를 + + if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 길찾기가 불가능한 상태라면 + + if (distToPlayer > attackRange) // 조건이 맞으면 실행할거에요 -> 플레이어가 공격 사거리 밖이라면 { - agent.SetDestination(target.position); + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고 + agent.SetDestination(target.position); // 명령을 내릴거에요 -> 길찾기 목표를 플레이어 위치로 + LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을 + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걷기 애니메이션을 맞추는 UpdateMovementAnimation을 + } + else // 조건이 틀리면 실행할거에요 -> 플레이어가 사거리 안에 있다면 + { + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 완전한 0으로 + LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을 - if (animator != null) animator.SetFloat("Speed", agent.velocity.magnitude); - - if (agent.remainingDistance <= agent.stoppingDistance + 0.5f) + if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 존재한다면 { - agent.isStopped = true; - LookAtTarget(target.position); - if (animator != null) animator.SetFloat("Speed", 0f); + AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를 + if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷기 모션이 재생 중이라면 + { + animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로 + } } - else + + if (_timer <= 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 0 이하라면 { - agent.isStopped = false; + _timer = patternInterval; // 값을 넣을거에요 -> 쿨타임을 다시 패턴 대기 시간으로 + Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 출력할거에요 -> 공격 시점과 거리 정보를 + DecideAttack(); // 함수를 실행할거에요 -> 어떤 공격을 할지 고르는 DecideAttack을 } } } - // ════════════════════════════════════════ - // 🏃‍♂️ 무기 회수 로직 - // ════════════════════════════════════════ - - private void RetrieveWeaponLogic() - { - if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; - - agent.SetDestination(ironBall.transform.position); - agent.isStopped = false; - - if (animator != null) animator.SetFloat("Speed", agent.velocity.magnitude); - - float dist = Vector3.Distance(transform.position, ironBall.transform.position); - - if (dist <= 3.0f && !isAttacking) - { - StartCoroutine(PickUpBallRoutine()); - } - } - - private IEnumerator PickUpBallRoutine() - { - OnAttackStart(); - - if (agent != null && agent.isOnNavMesh) - { - agent.isStopped = true; - agent.velocity = Vector3.zero; - } - - if (animator != null) - { - animator.SetFloat("Speed", 0); - animator.Play("Skill_Pickup"); - } - - yield return new WaitForSeconds(0.8f); - - ironBall.transform.SetParent(handHolder); - ironBall.transform.localPosition = Vector3.zero; - ironBall.transform.localRotation = Quaternion.identity; - - if (ballRb != null) - { - ballRb.isKinematic = true; - ballRb.velocity = Vector3.zero; - } - - yield return new WaitForSeconds(1.0f); - - isWeaponless = false; - OnAttackEnd(); - - if (agent != null && agent.isOnNavMesh) agent.isStopped = false; - } - // ════════════════════════════════════════ // 🎬 전투 입장 // ════════════════════════════════════════ - public void StartBossBattle() + public void StartBossBattle() // 함수를 선언할거에요 -> 외부에서 전투를 시작시킬 수 있는 StartBossBattle을 { - if (isBattleStarted) return; - StartCoroutine(BattleStartRoutine()); + if (isBattleStarted) return; // 조건이 맞으면 중단할거에요 -> 이미 전투가 시작되었다면 중복 실행을 + StartCoroutine(BattleStartRoutine()); // 코루틴을 실행할거에요 -> Roar 연출 후 전투를 시작하는 BattleStartRoutine을 } - private IEnumerator BattleStartRoutine() + private IEnumerator BattleStartRoutine() // 코루틴 함수를 선언할거에요 -> Roar 연출과 전투 시작을 담당하는 BattleStartRoutine을 { - Debug.Log("🔥 보스 전투 시작! (Roar)"); + Debug.Log("🔥 [Boss] Roar 시작!"); // 로그를 출력할거에요 -> Roar가 시작되었다는 메시지를 + isPerformingAction = true; // 상태를 바꿀거에요 -> 연출 중 플래그를 참(true)으로 - // ⭐ [핵심] 연출 중 플래그 ON → ExecuteAILogic이 아무것도 안 함 - isPerformingAction = true; + if (bossHealthBar != null) bossHealthBar.SetActive(true); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에 켜기를 + if (counterSystem != null) counterSystem.InitializeBattle(); // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 있다면 전투 초기화를 - if (bossHealthBar != null) bossHealthBar.SetActive(true); - if (counterSystem != null) counterSystem.InitializeBattle(); + if (animator != null) animator.Play(anim_Roar, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar 애니메이션을 처음부터 재생을 - // ⭐ [핵심] Roar 재생 - if (animator != null) + yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> Roar 애니메이션이 끝날 2초의 시간을 + + if (animator != null) animator.Play(anim_Idle, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar가 끝났으니 대기 모션으로 전환을 + + isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 중 플래그를 거짓(false)으로 + isBattleStarted = true; // 상태를 바꿀거에요 -> 전투 시작 플래그를 참(true)으로 + + Debug.Log("⚔️ [Boss] Roar 종료 → 전투 루프 시작!"); // 로그를 출력할거에요 -> 전투 루프가 시작되었다는 메시지를 + + if (agent != null) // 조건이 맞으면 실행할거에요 -> 길찾기 에이전트가 존재한다면 { - animator.Play("Roar", 0, 0f); - } - - // Roar 애니메이션 대기 (길이에 맞게 조절) - yield return new WaitForSeconds(2.0f); - - // ⭐ [핵심] Roar 끝나면 Idle로 전환 - if (animator != null) - { - animator.Play("Monster_Idle", 0, 0f); - } - - // 연출 종료 → 전투 시작 - isPerformingAction = false; - isBattleStarted = true; - - // NavMeshAgent 활성화 - if (agent != null) - { - agent.enabled = true; - - // NavMesh 위에 올라갈 때까지 잠시 대기 - int retry = 0; - while (!agent.isOnNavMesh && retry < 10) + agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를 활성화(true)로 + int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로 + while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> NavMesh에 안 닿았고 10번 미만인 동안 { - retry++; - yield return new WaitForSeconds(0.1f); + retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼 + yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을 } - if (agent.isOnNavMesh) + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 올라갔다면 { - agent.isStopped = false; + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 추격을 시작하라고 + Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 출력할거에요 -> NavMesh 연결 성공 메시지를 + } + else // 조건이 틀리면 실행할거에요 -> NavMesh에 못 올라갔다면 + { + Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 출력할거에요 -> 연결 실패 경고를 } } - - Debug.Log("⚔️ 보스 전투 루프 시작!"); } - private void LookAtTarget(Vector3 targetPos) + // ════════════════════════════════════════ + // 🚶 이동 애니메이션 & 타겟팅 + // ════════════════════════════════════════ + + private void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 상태에 맞는 애니메이션을 재생하는 UpdateMovementAnimation을 { - Vector3 dir = targetPos - transform.position; - dir.y = 0; - if (dir != Vector3.zero) + if (animator == null || isPerformingAction || isAttacking || isHit || isDead) return; // 조건이 맞으면 중단할거에요 -> 애니메이션을 바꿀 수 없는 상태라면 + + AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를 + + if (agent != null && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 현재 이동 속도가 0.1보다 크다면 { - transform.rotation = Quaternion.Slerp( - transform.rotation, - Quaternion.LookRotation(dir), - Time.deltaTime * 5f + if (!state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 아직 걷기 모션이 아니라면 + animator.Play(anim_Walk); // 애니메이션을 바꿀거에요 -> 걷기 모션으로 + } + else // 조건이 틀리면 실행할거에요 -> 거의 멈춰있다면 + { + if (!state.IsName(anim_Idle) && !state.IsName(anim_Roar)) // 조건이 맞으면 실행할거에요 -> 대기나 포효 모션이 아니라면 + animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로 + } + } + + private void LookAtTarget(Vector3 targetPos) // 함수를 선언할거에요 -> 타겟을 부드럽게 쳐다보는 LookAtTarget을 + { + Vector3 dir = targetPos - transform.position; // 벡터를 계산할거에요 -> 내 위치에서 타겟까지의 방향을 + dir.y = 0; // 값을 바꿀거에요 -> 위아래로 고개가 꺾이지 않게 y를 0으로 + if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 0이 아니라면 + { + transform.rotation = Quaternion.Slerp( // 회전시킬거에요 -> 현재 회전에서 타겟 방향으로 부드럽게 + transform.rotation, // 시작값으로 사용할거에요 -> 현재 회전값을 + Quaternion.LookRotation(dir), // 목표값으로 사용할거에요 -> 타겟을 향하는 회전값을 + Time.deltaTime * 5f // 속도를 지정할거에요 -> 프레임 시간의 5배 속도로 ); } } // ════════════════════════════════════════ - // 🧠 AI 공격 판단 + // 🧠 공격 판단 // ════════════════════════════════════════ - private void DecideAttack() + private void DecideAttack() // 함수를 선언할거에요 -> 어떤 공격 패턴을 쓸지 고르는 DecideAttack을 { - if (animator != null) animator.SetFloat("Speed", 0f); + if (target != null) LookAtTarget(target.position); // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 그쪽을 바라보기를 - // 공격 전 이동 멈춤 - if (agent != null && agent.isOnNavMesh) + string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 변수를 선언할거에요 -> 카운터 시스템에서 받은 패턴 이름을 patternName에 + Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 출력할거에요 -> 선택된 패턴 이름을 + + switch (patternName) // 분기할거에요 -> 받아온 패턴 이름에 따라서 { - agent.isStopped = true; - agent.velocity = Vector3.zero; - } - - // 플레이어를 바라봄 - if (target != null) LookAtTarget(target.position); - - string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; - - Debug.Log($"🧠 보스 패턴 선택: {patternName}"); - - switch (patternName) - { - case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; - case "Smash": StartCoroutine(Pattern_SmashAttack()); break; - case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; - default: StartCoroutine(Pattern_ThrowBall()); break; + case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 일치하면 실행할거에요 -> 돌진 공격을 + case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 일치하면 실행할거에요 -> 내려찍기 공격을 + case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 일치하면 실행할거에요 -> 휩쓸기 공격을 + default: StartCoroutine(Pattern_ThrowBall()); break; // 맞는게 없으면 실행할거에요 -> 공 던지기 공격을 } } // ════════════════════════════════════════ - // ⚔️ 공격 패턴 + // 🏃 무기 회수 // ════════════════════════════════════════ - // 1. 돌진 - private IEnumerator Pattern_DashAttack() + private void RetrieveWeaponLogic() // 함수를 선언할거에요 -> 던진 쇠공을 주우러 가는 RetrieveWeaponLogic을 { - OnAttackStart(); + if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 쇠공이 없거나 길찾기가 불가능하다면 - if (agent != null) agent.enabled = false; + agent.SetDestination(ironBall.transform.position); // 명령을 내릴거에요 -> 길찾기 목표를 쇠공의 위치로 + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고 + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걸어갈 때 이동 애니메이션이 나오도록 - if (animator != null) animator.Play("Skill_Dash_Ready", 0, 0f); - yield return new WaitForSeconds(0.5f); - - // 돌진 - if (rb != null) - { - rb.isKinematic = false; - rb.velocity = transform.forward * 20f; - } - - if (animator != null) animator.Play("Skill_Dash_Go", 0, 0f); - yield return new WaitForSeconds(1.0f); - - // 정지 - if (rb != null) - { - rb.velocity = Vector3.zero; - rb.isKinematic = true; - } - - // NavMeshAgent 복구 - if (agent != null && isBattleStarted) - { - agent.enabled = true; - // NavMesh 복귀 대기 - int retry = 0; - while (agent != null && !agent.isOnNavMesh && retry < 10) - { - retry++; - yield return new WaitForSeconds(0.1f); - } - } - - OnAttackEnd(); + if (Vector3.Distance(transform.position, ironBall.transform.position) <= 3.0f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 쇠공과 3m 이내이고 공격 중이 아니라면 + StartCoroutine(PickUpBallRoutine()); // 코루틴을 실행할거에요 -> 바닥의 쇠공을 줍는 PickUpBallRoutine을 } - // 2. 스매시 - private IEnumerator Pattern_SmashAttack() + private IEnumerator PickUpBallRoutine() // 코루틴 함수를 선언할거에요 -> 쇠공 줍기 행동인 PickUpBallRoutine을 { - OnAttackStart(); + OnAttackStart(); // 상태를 켤거에요 -> 다른 행동과 겹치지 않게 공격 플래그를 + if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 줍기 위해 이동을 멈추고 속도를 0으로 + if (animator != null) animator.Play(anim_Pickup); // 조건이 맞으면 실행할거에요 -> 줍는 애니메이션을 + yield return new WaitForSeconds(0.8f); // 기다릴거에요 -> 손이 바닥에 닿을 0.8초를 - if (animator != null) animator.Play("Skill_Smash_Impact", 0, 0f); - yield return new WaitForSeconds(1.2f); + ironBall.transform.SetParent(handHolder); // 부모를 바꿀거에요 -> 쇠공을 보스의 손 밑으로 + ironBall.transform.localPosition = Vector3.zero; // 위치를 바꿀거에요 -> 쇠공의 위치를 손의 정중앙으로 + ironBall.transform.localRotation = Quaternion.identity; // 회전을 바꿀거에요 -> 쇠공의 회전을 기본값으로 + if (ballRb != null) { ballRb.isKinematic = true; ballRb.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 쇠공의 물리를 끄고 속도를 0으로 - if (animator != null) animator.Play("Skill_Smash_Impact", 0, 0f); - yield return new WaitForSeconds(1.0f); - - OnAttackEnd(); - } - - // 3. 쓸기 - private IEnumerator Pattern_ShieldWall() - { - OnAttackStart(); - - if (animator != null) animator.Play("Skill_Sweep", 0, 0f); - yield return new WaitForSeconds(2.0f); - - OnAttackEnd(); - } - - // 4. 공 던지기 - private IEnumerator Pattern_ThrowBall() - { - OnAttackStart(); - - if (animator != null) animator.Play("Attack_Throw", 0, 0f); - yield return new WaitForSeconds(0.5f); - - if (ironBall != null && ballRb != null) - { - ironBall.transform.SetParent(null); - ballRb.isKinematic = false; - - Vector3 dir = (target.position - transform.position).normalized; - ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); - ballRb.angularDrag = 5f; - } - - isWeaponless = true; - - yield return new WaitForSeconds(1.0f); - OnAttackEnd(); + yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 일어서는 애니메이션이 끝날 1초를 + isWeaponless = false; // 상태를 바꿀거에요 -> 무기를 다시 쥐었으므로 맨손 상태를 거짓으로 + OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 처리를 + if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 다시 추격을 위해 이동 정지 해제를 } // ════════════════════════════════════════ - // ⭐ [신규] 피격 시 부모 클래스 StopAllCoroutines 대응 + // ✅ 공격 시작/종료 처리 (핵심) // ════════════════════════════════════════ - /// - /// 부모의 StartHit()이 StopAllCoroutines()를 호출하므로, - /// 보스 전용 상태를 여기서 복구합니다. - /// - protected override void OnStartHit() + public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 처리를 하는 OnAttackStart를 { - isPerformingAction = false; // 연출 중이었으면 해제 - - // 공격 중이었으면 해제 - if (isAttacking) - { - isAttacking = false; - if (myWeapon != null) myWeapon.DisableHitBox(); - } - - // 돌진 중 물리 복구 - if (rb != null && !rb.isKinematic) - { - rb.velocity = Vector3.zero; - rb.isKinematic = true; - } + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중임을 참으로 + isPerformingAction = true; // 상태를 바꿀거에요 -> 다른 행동 불가 상태로 + if (agent != null) agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 } - /// - /// 피격 종료 후 NavMeshAgent 복구 - /// - public override void OnHitEnd() + public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를 { - base.OnHitEnd(); + StartCoroutine(RecoverRoutine()); // 코루틴을 시작할거에요 -> 후딜레이(회복) 처리를 위한 RecoverRoutine을 + } - // 전투 중이면 Agent 다시 활성화 - if (isBattleStarted && agent != null && !agent.enabled) - { - agent.enabled = true; - } + private IEnumerator RecoverRoutine() // 코루틴 함수를 선언할거에요 -> 공격 후 대기 시간을 갖는 RecoverRoutine을 + { + isAttacking = false; // 상태를 바꿀거에요 -> 공격이 끝났으므로 공격 중 상태를 거짓으로 + isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참으로 (이때는 추격 안 함) + + if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 대기 모션으로 전환을 + + // 딜레이 시간 (여기서 patternInterval 만큼 멍때림) + yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 패턴 간격 시간만큼 + + isResting = false; // 상태를 바꿀거에요 -> 휴식이 끝났으므로 거짓으로 + isPerformingAction = false; // 상태를 바꿀거에요 -> 이제 다시 다른 행동이 가능하도록 거짓으로 } // ════════════════════════════════════════ - // ⭐ [신규] 사망 처리 + // ⚔️ 공격 패턴들 // ════════════════════════════════════════ - protected override void OnDie() + // 패턴 1: 돌진 공격 + private IEnumerator Pattern_DashAttack() // 코루틴 함수를 선언할거에요 -> 돌진 패턴인 Pattern_DashAttack을 { - isBattleStarted = false; - isPerformingAction = false; - isWeaponless = false; + OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 + if (agent != null) agent.enabled = false; // 조건이 맞으면 실행할거에요 -> 돌진 시 물리력을 위해 길찾기를 끄기로 - // 물리 정리 - if (rb != null) + if (animator != null) animator.Play(anim_DashReady, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 준비 모션을 + yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 준비 포즈를 취할 0.5초를 + + if (rb != null) { rb.isKinematic = false; rb.velocity = transform.forward * 20f; } // 조건이 맞으면 실행할거에요 -> 물리를 켜고 앞으로 속도 20으로 돌진을 + if (animator != null) animator.Play(anim_DashGo, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 애니메이션을 + yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 돌진이 끝날 1초를 + + if (rb != null) { rb.velocity = Vector3.zero; rb.isKinematic = true; } // 조건이 맞으면 실행할거에요 -> 속도를 0으로 멈추고 물리를 끄기로 + if (agent != null && isBattleStarted) agent.enabled = true; // 조건이 맞으면 실행할거에요 -> 전투 중이면 길찾기를 다시 켜기로 + OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 + } + + // 패턴 2: 내려찍기 + private IEnumerator Pattern_SmashAttack() // 코루틴 함수를 선언할거에요 -> 내려찍기 패턴인 Pattern_SmashAttack을 + { + OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 + if (animator != null) animator.Play(anim_SmashCharge, 0, 0f); // 조건이 맞으면 실행할거에요 -> 기를 모으는 모션을 + yield return new WaitForSeconds(1.2f); // 기다릴거에요 -> 기를 모을 1.2초를 + if (animator != null) animator.Play(anim_SmashImpact, 0, 0f); // 조건이 맞으면 실행할거에요 -> 내려찍는 모션을 + yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 자세를 가다듬을 1초를 + OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 + } + + // 패턴 3: 휩쓸기 + private IEnumerator Pattern_ShieldWall() // 코루틴 함수를 선언할거에요 -> 휩쓸기 패턴인 Pattern_ShieldWall을 + { + OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 + if (animator != null) animator.Play(anim_Sweep, 0, 0f); // 조건이 맞으면 실행할거에요 -> 휩쓰는 모션을 + yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> 휘두르기가 끝날 2초를 + OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 + } + + // 패턴 4: 공 던지기 + private IEnumerator Pattern_ThrowBall() // 코루틴 함수를 선언할거에요 -> 쇠공 던지기 패턴인 Pattern_ThrowBall을 + { + OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 + if (animator != null) animator.Play(anim_Throw, 0, 0f); // 조건이 맞으면 실행할거에요 -> 던지는 모션을 + yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 공을 던지기 직전인 0.5초를 + + if (ironBall != null && ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공과 물리 엔진이 존재한다면 { - rb.velocity = Vector3.zero; - rb.isKinematic = true; + ironBall.transform.SetParent(null); // 부모를 바꿀거에요 -> 쇠공을 손에서 완전히 분리로 + ballRb.isKinematic = false; // 기능을 켤거에요 -> 쇠공의 물리 연산을 활성화로 + Vector3 dir = (target.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을 + ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); // 힘을 가할거에요 -> 앞으로 20, 위로 5의 포물선 힘을 + ballRb.angularDrag = 5f; // 마찰을 줄거에요 -> 쇠공이 멀리 안 굴러가게 회전 마찰 5를 } - // 체력바 숨기기 - if (bossHealthBar != null) bossHealthBar.SetActive(false); + isWeaponless = true; // 상태를 바꿀거에요 -> 던졌으므로 맨손 상태를 참으로 + yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 던지고 자세를 가다듬을 1초를 + OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 + } + + // ════════════════════════════════════════ + // 💥 피격 / 사망 + // ════════════════════════════════════════ + + protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 보스가 맞았을 때 호출되는 OnStartHit을 + { + // 보스는 피격되어도 행동이 끊기지 않습니다 (슈퍼아머) + // 만약 끊기게 하려면 여기서 isPerformingAction = false; + } + + protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 보스 체력이 0이 되면 호출되는 OnDie를 + { + isBattleStarted = false; // 상태를 바꿀거에요 -> 전투가 끝났으므로 전투 플래그를 거짓으로 + isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 플래그를 거짓으로 + if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를 + GiveBossXP(); // 함수를 실행할거에요 -> 경험치를 주는 GiveBossXP를 + base.OnDie(); // 부모 클래스의 사망 처리를 호출할거에요 -> 애니메이션, 시체 제거 등 + } + + private void GiveBossXP() // 함수를 선언할거에요 -> 보스 처치 보상을 주는 GiveBossXP를 + { + Debug.Log("[BossController] 보스 처치 시도 중..."); // 로그를 출력할거에요 -> 보스 처치 시도 메시지를 + if (ObsessionSystem.instance != null) // 조건이 맞으면 실행할거에요 -> 경험치 시스템이 존재한다면 + ObsessionSystem.instance.AddRunXP(100); // 함수를 실행할거에요 -> 경험치 100을 주는 AddRunXP를 + else // 조건이 틀리면 실행할거에요 -> 시스템을 못 찾았다면 + Debug.LogError("[BossController] ObsessionSystem 인스턴스를 찾을 수 없습니다!"); // 에러를 출력할거에요 -> 시스템 누락 경고를 + Debug.Log("[BossController] 보스 처치 처리 완료!"); // 로그를 출력할거에요 -> 빨간 글씨로 처치 완료 메시지를 } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs b/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs index 2d4677dc..1c5ee9ea 100644 --- a/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs +++ b/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs @@ -1,29 +1,29 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class BossZoneTrigger : MonoBehaviour +public class BossZoneTrigger : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossZoneTrigger를 { - [SerializeField] private NorcielBoss boss; // - [SerializeField] private GameObject fogWall; // Ա Ȱ () + [SerializeField] private NorcielBoss boss; // 변수를 선언할거에요 -> 깨울 보스 스크립트(NorcielBoss)를 boss에 + [SerializeField] private GameObject fogWall; // 변수를 선언할거에요 -> 입구를 막을 안개벽 오브젝트를 fogWall에 - private bool hasTriggered = false; + private bool hasTriggered = false; // 변수를 초기화할거에요 -> 이미 발동했는지 여부를 거짓(false)으로 - private void OnTriggerEnter(Collider other) + private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 무언가가 트리거에 닿았을 때 OnTriggerEnter를 { - // ̹ ߵ߰ų, ÷̾ ƴϸ - if (hasTriggered) return; + // 이미 발동했거나, 플레이어가 아니면 무시 + if (hasTriggered) return; // 조건이 맞으면 중단할거에요 -> 이미 발동했다면(hasTriggered가 참이면) - if (other.CompareTag("Player")) + if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 닿은 물체의 태그가 "Player"라면 { - hasTriggered = true; + hasTriggered = true; // 상태를 바꿀거에요 -> 발동 상태를 참(true)으로 - // 1. - if (boss != null) boss.StartBossBattle(); + // 1. 보스 깨우기 + if (boss != null) boss.StartBossBattle(); // 조건이 맞으면 실행할거에요 -> 보스가 연결되어 있다면 전투 시작 함수(StartBossBattle)를 - // 2. Ա () - if (fogWall != null) fogWall.SetActive(true); + // 2. 도망 못 가게 입구 막기 (선택) + if (fogWall != null) fogWall.SetActive(true); // 조건이 맞으면 실행할거에요 -> 안개벽이 연결되어 있다면 활성화(true)를 - // 3. ƮŴ - // gameObject.SetActive(false); // ٷ Ȱ + // 3. 이 트리거는 할 일 다 했으니 끄기 + // gameObject.SetActive(false); // 바로 끄면 안개도 꺼질 수 있으니 주의 } } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/CounterType.cs b/Assets/Scripts/Enemy/BossAI/CounterType.cs index cab44c60..5f60e7e9 100644 --- a/Assets/Scripts/Enemy/BossAI/CounterType.cs +++ b/Assets/Scripts/Enemy/BossAI/CounterType.cs @@ -1,10 +1,10 @@ -/// -/// 보스 카운터 패턴 타입 열거형 -/// -public enum CounterType -{ - None, - Dodge, // 회피 남발 카운터 - Aim, // 정조준 과다 카운터 - Pierce // 관통 화살 편중 카운터 -} +/// // 요약 설명을 적을거에요 -> 아래 enum이 뭔지 +/// 보스 카운터 패턴 타입 열거형 // 설명할거에요 -> 보스가 어떤 카운터 패턴을 쓰는지 구분하는 타입 +/// // 요약 설명을 끝낼거에요 -> 주석 블록을 +public enum CounterType // 열거형을 선언할거에요 -> CounterType이라는 이름의 enum을 +{ // 코드 블록을 시작할거에요 -> CounterType 범위를 + None, // 값을 정의할거에요 -> 카운터 없음 상태를 + Dodge, // 값을 정의할거에요 -> 회피 남발 카운터 타입을 + Aim, // 값을 정의할거에요 -> 정조준 과다 카운터 타입을 + Pierce // 값을 정의할거에요 -> 관통 화살 편중 카운터 타입을 +} // 코드 블록을 끝낼거에요 -> CounterType을 \ No newline at end of file diff --git a/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs b/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs index fd21dbfe..917e48a8 100644 --- a/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs +++ b/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs @@ -1,4 +1,4 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 /// /// 하이브리드 몬스터 스포너 (생성은 스포너 기준, 생존은 몬스터 기준) @@ -6,52 +6,52 @@ /// - Life & Hibernate: 몬스터 기준 거리로 최적화 /// - Wake Up: 범위 안으로 복귀 시 재활성화 /// -public class MonsterSpawner : MonoBehaviour +public class MonsterSpawner : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterSpawner를 { - [Header("=== 생성 설정 (Birth - 스포너 기준) ===")] - [Tooltip("스포너로부터 이 거리 안에 플레이어가 들어오면 몬스터 생성")] - [SerializeField] private float spawnRange = 25f; + [Header("=== 생성 설정 (Birth - 스포너 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 생성 설정 (Birth - 스포너 기준) === 을 + [Tooltip("스포너로부터 이 거리 안에 플레이어가 들어오면 몬스터 생성")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float spawnRange = 25f; // 변수를 선언할거에요 -> 몬스터 생성 거리(25.0)를 spawnRange에 - [Tooltip("몬스터가 죽은 후 재생성까지 대기 시간")] - [SerializeField] private float respawnCooldown = 3f; + [Tooltip("몬스터가 죽은 후 재생성까지 대기 시간")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float respawnCooldown = 3f; // 변수를 선언할거에요 -> 재소환 대기시간(3초)을 respawnCooldown에 - [Tooltip("오브젝트 풀에서 가져올 몬스터 태그")] - [SerializeField] private string mobTag = "NormalMob"; + [Tooltip("오브젝트 풀에서 가져올 몬스터 태그")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private string mobTag = "NormalMob"; // 변수를 선언할거에요 -> 소환할 몬스터의 태그 이름("NormalMob")을 mobTag에 - [Header("=== 최적화 설정 (Life & Hibernate - 몬스터 기준) ===")] - [Tooltip("몬스터 본체로부터 이 거리를 벗어나면 강제 동면 (전투 중이어도!)")] - [SerializeField] private float optimizationRange = 60f; + [Header("=== 최적화 설정 (Life & Hibernate - 몬스터 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 최적화 설정 (Life & Hibernate - 몬스터 기준) === 을 + [Tooltip("몬스터 본체로부터 이 거리를 벗어나면 강제 동면 (전투 중이어도!)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float optimizationRange = 60f; // 변수를 선언할거에요 -> 몬스터 최적화(동면) 거리(60.0)를 optimizationRange에 // 내부 상태 - private GameObject _myMonster; - private MonsterClass _monsterScript; - private Transform _player; - private float _nextSpawnTime; + private GameObject _myMonster; // 변수를 선언할거에요 -> 소환된 몬스터 게임오브젝트를 담을 _myMonster를 + private MonsterClass _monsterScript; // 변수를 선언할거에요 -> 몬스터의 스크립트(MonsterClass)를 담을 _monsterScript를 + private Transform _player; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 _player를 + private float _nextSpawnTime; // 변수를 선언할거에요 -> 다음 소환 가능 시간을 저장할 _nextSpawnTime을 - private void Start() + private void Start() // 함수를 실행할거에요 -> 게임 시작 시 호출되는 Start를 { - FindPlayer(); + FindPlayer(); // 함수를 실행할거에요 -> 플레이어를 찾는 FindPlayer를 } - private void FindPlayer() + private void FindPlayer() // 함수를 선언할거에요 -> 플레이어를 찾아 변수에 저장하는 FindPlayer를 { - GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); - if (playerObj != null) _player = playerObj.transform; + GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); // 오브젝트를 찾을거에요 -> "Player" 태그가 붙은 오브젝트를 playerObj에 + if (playerObj != null) _player = playerObj.transform; // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면 그 위치 정보를 _player에 저장 } - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 { - if (_player == null) { FindPlayer(); return; } + if (_player == null) { FindPlayer(); return; } // 조건이 맞으면 실행할거에요 -> 플레이어 정보가 없다면 다시 찾고 이번 프레임은 중단(return) // 몬스터가 없거나 죽었으면 -> Birth - if (_myMonster == null || (_monsterScript != null && _monsterScript.IsDead)) + if (_myMonster == null || (_monsterScript != null && _monsterScript.IsDead)) // 조건이 맞으면 실행할거에요 -> 몬스터가 없거나, 몬스터 스크립트가 있는데 죽은 상태라면 { - HandleBirth(); + HandleBirth(); // 함수를 실행할거에요 -> 몬스터 생성 로직인 HandleBirth를 } // 몬스터가 살아있으면 -> Life & Hibernate - else + else // 조건이 틀리면 실행할거에요 -> 몬스터가 살아서 활동 중이라면 { - HandleLifeAndHibernate(); + HandleLifeAndHibernate(); // 함수를 실행할거에요 -> 생존 및 최적화(동면) 로직인 HandleLifeAndHibernate를 } } @@ -59,25 +59,25 @@ public class MonsterSpawner : MonoBehaviour // 🐣 BIRTH (생성) - 스포너 기준 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - private void HandleBirth() + private void HandleBirth() // 함수를 선언할거에요 -> 몬스터 생성 조건을 확인하는 HandleBirth를 { // ⭐ 스포너 <-> 플레이어 거리 - float distanceFromSpawner = Vector3.Distance(transform.position, _player.position); + float distanceFromSpawner = Vector3.Distance(transform.position, _player.position); // 값을 계산할거에요 -> 스포너와 플레이어 사이의 거리를 distanceFromSpawner에 - if (distanceFromSpawner <= spawnRange && Time.time >= _nextSpawnTime) + if (distanceFromSpawner <= spawnRange && Time.time >= _nextSpawnTime) // 조건이 맞으면 실행할거에요 -> 거리가 생성 범위 이내이고, 쿨타임 시간이 지났다면 { - SpawnMonster(); + SpawnMonster(); // 함수를 실행할거에요 -> 실제 몬스터를 소환하는 SpawnMonster를 } } - private void SpawnMonster() + private void SpawnMonster() // 함수를 선언할거에요 -> 오브젝트 풀에서 몬스터를 가져오는 SpawnMonster를 { - _myMonster = GenericObjectPool.Instance.SpawnFromPool(mobTag, transform.position, transform.rotation); + _myMonster = GenericObjectPool.Instance.SpawnFromPool(mobTag, transform.position, transform.rotation); // 오브젝트를 가져올거에요 -> 풀(Pool)에서 mobTag에 맞는 몬스터를 스포너 위치에 소환해서 _myMonster에 - if (_myMonster != null) + if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 소환에 성공했다면 { - _monsterScript = _myMonster.GetComponent(); - if (_monsterScript != null) _monsterScript.ResetStats(); + _monsterScript = _myMonster.GetComponent(); // 컴포넌트를 가져올거에요 -> 몬스터의 핵심 스크립트(MonsterClass)를 _monsterScript에 + if (_monsterScript != null) _monsterScript.ResetStats(); // 조건이 맞으면 실행할거에요 -> 스크립트가 있다면 몬스터의 상태(체력 등)를 초기화(ResetStats) } } @@ -85,43 +85,43 @@ public class MonsterSpawner : MonoBehaviour // 💤 LIFE & HIBERNATE (생존/동면) - 몬스터 기준 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - private void HandleLifeAndHibernate() + private void HandleLifeAndHibernate() // 함수를 선언할거에요 -> 몬스터의 활동 및 최적화를 관리하는 HandleLifeAndHibernate를 { - if (_myMonster == null) { _monsterScript = null; return; } + if (_myMonster == null) { _monsterScript = null; return; } // 조건이 맞으면 실행할거에요 -> 몬스터 오브젝트가 사라졌다면 스크립트 변수도 비우고 중단(return) // ⭐ 몬스터 본체 <-> 플레이어 거리 - float distanceFromMonster = Vector3.Distance(_myMonster.transform.position, _player.position); + float distanceFromMonster = Vector3.Distance(_myMonster.transform.position, _player.position); // 값을 계산할거에요 -> 몬스터와 플레이어 사이의 거리를 distanceFromMonster에 - if (distanceFromMonster > optimizationRange) + if (distanceFromMonster > optimizationRange) // 조건이 맞으면 실행할거에요 -> 몬스터와 플레이어 거리가 최적화 범위(optimizationRange)보다 멀다면 { // 😴 HIBERNATE (동면) - HibernateMonster(); + HibernateMonster(); // 함수를 실행할거에요 -> 몬스터를 잠재우는 HibernateMonster를 } - else + else // 조건이 틀리면 실행할거에요 -> 거리가 가깝다면 { // 👁 WAKE UP (기상) - WakeUpMonster(); + WakeUpMonster(); // 함수를 실행할거에요 -> 몬스터를 깨우는 WakeUpMonster를 } } - private void HibernateMonster() + private void HibernateMonster() // 함수를 선언할거에요 -> 몬스터를 비활성화하는 HibernateMonster를 { - if (_myMonster.activeSelf) + if (_myMonster.activeSelf) // 조건이 맞으면 실행할거에요 -> 몬스터가 현재 켜져 있는(Active) 상태라면 { - _myMonster.SetActive(false); + _myMonster.SetActive(false); // 기능을 끌거에요 -> 몬스터 오브젝트를 비활성화(false)로 } } - private void WakeUpMonster() + private void WakeUpMonster() // 함수를 선언할거에요 -> 몬스터를 다시 활성화하는 WakeUpMonster를 { - if (!_myMonster.activeSelf) + if (!_myMonster.activeSelf) // 조건이 맞으면 실행할거에요 -> 몬스터가 현재 꺼져 있는(Inactive) 상태라면 { - _myMonster.SetActive(true); + _myMonster.SetActive(true); // 기능을 켤거에요 -> 몬스터 오브젝트를 활성화(true)로 - if (_monsterScript != null) + if (_monsterScript != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 존재한다면 { // ⭐ AI 복구 (전투 중이었다면 추적 재개) - _monsterScript.Reactivate(); + _monsterScript.Reactivate(); // 함수를 실행할거에요 -> 몬스터의 AI를 다시 작동시키는 Reactivate를 } } } @@ -133,31 +133,31 @@ public class MonsterSpawner : MonoBehaviour /// /// 몬스터가 죽었을 때 호출 (쿨타임 적용) /// - public void NotifyMonsterDead() + public void NotifyMonsterDead() // 함수를 선언할거에요 -> 외부에서 몬스터 사망을 알릴 때 쓰는 NotifyMonsterDead를 { // 쿨타임 계산: 현재 시간 + 대기 시간 - _nextSpawnTime = Time.time + respawnCooldown; + _nextSpawnTime = Time.time + respawnCooldown; // 값을 저장할거에요 -> 현재 시간에 쿨타임을 더해서 다음 소환 시간(_nextSpawnTime)으로 // 몬스터 참조를 비워서 Update에서 다시 HandleBirth()로 넘어가게 함 - _myMonster = null; - _monsterScript = null; + _myMonster = null; // 값을 비울거에요 -> 소환된 몬스터 변수를 null로 + _monsterScript = null; // 값을 비울거에요 -> 몬스터 스크립트 변수를 null로 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Gizmos // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - private void OnDrawGizmosSelected() + private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 오브젝트 선택 시 기즈모를 그리는 OnDrawGizmosSelected를 { // 스포너 생성 범위 (빨간색) - Gizmos.color = Color.red; - Gizmos.DrawWireSphere(transform.position, spawnRange); + Gizmos.color = Color.red; // 색상을 설정할거에요 -> 기즈모 색을 빨간색으로 + Gizmos.DrawWireSphere(transform.position, spawnRange); // 그림을 그릴거에요 -> 스포너 위치에 생성 범위(spawnRange) 크기의 원을 // 몬스터 최적화 범위 (파란색) - if (_myMonster != null) + if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 소환된 몬스터가 있다면 { - Gizmos.color = Color.blue; - Gizmos.DrawWireSphere(_myMonster.transform.position, optimizationRange); + Gizmos.color = Color.blue; // 색상을 설정할거에요 -> 기즈모 색을 파란색으로 + Gizmos.DrawWireSphere(_myMonster.transform.position, optimizationRange); // 그림을 그릴거에요 -> 몬스터 위치에 최적화 범위(optimizationRange) 크기의 원을 } } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs b/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs index 2af849c2..90d01c5d 100644 --- a/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs +++ b/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs @@ -1,24 +1,24 @@ -using System.Collections.Generic; -using UnityEngine; +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class MobUpdateManager : MonoBehaviour +public class MobUpdateManager : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MobUpdateManager를 { - public static MobUpdateManager Instance; + public static MobUpdateManager Instance; // 변수를 선언할거에요 -> 어디서든 접근 가능한 싱글톤 인스턴스 Instance를 // ⭐ Monster -> MonsterClass로 수정하여 에러 해결 - private List _activeMobs = new List(); + private List _activeMobs = new List(); // 리스트를 초기화할거에요 -> 활성화된 몬스터들을 담을 _activeMobs를 - private void Awake() => Instance = this; + private void Awake() => Instance = this; // 함수를 실행할거에요 -> 스크립트 시작 시 내 자신(this)을 Instance에 넣기를 // ⭐ 매개변수 타입도 MonsterClass로 변경 - public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob); - public void UnregisterMob(MonsterClass mob) => _activeMobs.Remove(mob); + public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에 추가하는 RegisterMob을 + public void UnregisterMob(MonsterClass mob) => _activeMobs.Remove(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에서 제거하는 UnregisterMob을 - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 { - for (int i = 0; i < _activeMobs.Count; i++) + for (int i = 0; i < _activeMobs.Count; i++) // 반복할거에요 -> 리스트에 있는 모든 몬스터 개수만큼 { // 리스트를 돌며 카메라 최적화 로직이 담긴 OnManagedUpdate 호출 - if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate(); + if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate(); // 조건이 맞으면 실행할거에요 -> 몬스터가 존재한다면 관리 업데이트 함수(OnManagedUpdate)를 } } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/Types/DummyBot.cs b/Assets/Scripts/Enemy/Types/DummyBot.cs index 88ef8e9c..4ad5b5f8 100644 --- a/Assets/Scripts/Enemy/Types/DummyBot.cs +++ b/Assets/Scripts/Enemy/Types/DummyBot.cs @@ -1,35 +1,37 @@ -using UnityEngine; -using System; // ⭐ Action 사용을 위해 필요 +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 +using System; // Action 이벤트를 쓰기 위해 불러올거에요 -> System을 -public class TrainingDummy : MonoBehaviour, IDamageable -{ - [Header("더미 설정")] - [SerializeField] private float maxHP = 9999f; - private float _currentHP; +public class TrainingDummy : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> 더미 오브젝트가 데미지를 받게 만들 TrainingDummy를 +{ // 코드 블록을 시작할거에요 -> TrainingDummy 범위를 - private Animator _animator; + [Header("더미 설정")] // 인스펙터에 제목을 표시할거에요 -> 더미 설정을 + [SerializeField] private float maxHP = 9999f; // 변수를 선언할거에요 -> 최대 체력을 9999로 설정하는 maxHP를 + private float _currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHP를 - // ⭐ UI가 이 신호를 듣고 게이지를 줄입니다. - public event Action OnHealthChanged; + private Animator _animator; // 변수를 선언할거에요 -> 애니메이터 컴포넌트를 담을 _animator를 - private void Start() - { - _currentHP = maxHP; - _animator = GetComponent(); - } + // ⭐ UI가 이 신호를 듣고 게이지를 줄입니다. // 설명을 적을거에요 -> UI 업데이트용 이벤트임을 + public event Action OnHealthChanged; // 이벤트를 선언할거에요 -> (현재체력, 최대체력) 알림을 보내는 OnHealthChanged를 - public void TakeDamage(float amount) - { - _currentHP -= amount; + private void Start() // 함수를 선언할거에요 -> 시작할 때 1번 실행되는 Start를 + { // 코드 블록을 시작할거에요 -> Start 범위를 + _currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로 초기화 + _animator = GetComponent(); // 컴포넌트를 가져올거에요 -> Animator를 찾아 _animator에 + } // 코드 블록을 끝낼거에요 -> Start를 - // ⭐ UI에 현재 체력과 최대 체력 정보를 보냅니다. - OnHealthChanged?.Invoke(_currentHP, maxHP); + public void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 주면 호출되는 TakeDamage를 + { // 코드 블록을 시작할거에요 -> TakeDamage 범위를 + _currentHP -= amount; // 값을 뺄거에요 -> 현재 체력에서 데미지만큼 감소 - Debug.Log($"[더미] 피격! 데미지: {amount} | 남은 체력: {_currentHP}"); + // ⭐ UI에 현재 체력과 최대 체력 정보를 보냅니다. // 설명을 적을거에요 -> UI 갱신 트리거임을 + OnHealthChanged?.Invoke(_currentHP, maxHP); // 이벤트를 호출할거에요 -> UI에게 체력 변화 전달 - if (_animator != null) - { - _animator.Play("Hit", 0, 0f); - } - } -} \ No newline at end of file + Debug.Log($"[더미] 피격! 데미지: {amount} | 남은 체력: {_currentHP}"); // 로그를 찍을거에요 -> 데미지/남은 체력 확인용 + + if (_animator != null) // 조건을 검사할거에요 -> 애니메이터가 있는지 + { // 코드 블록을 시작할거에요 -> 애니메이션 재생 처리 + _animator.Play("Hit", 0, 0f); // 애니메이션을 재생할거에요 -> Hit 상태를 0번 레이어에서 처음부터 + } // 코드 블록을 끝낼거에요 -> 애니메이션 처리 + } // 코드 블록을 끝낼거에요 -> TakeDamage를 + +} // 코드 블록을 끝낼거에요 -> TrainingDummy를 \ No newline at end of file diff --git a/Assets/Scripts/Items/Pickups/HealthPotion.cs b/Assets/Scripts/Items/Pickups/HealthPotion.cs index 2375dc09..a6b16817 100644 --- a/Assets/Scripts/Items/Pickups/HealthPotion.cs +++ b/Assets/Scripts/Items/Pickups/HealthPotion.cs @@ -1,18 +1,20 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class HealthPotion : MonoBehaviour -{ - [Header("--- ---")] - [SerializeField] private float healAmount = 30f; // ȸ +public class HealthPotion : MonoBehaviour // 클래스를 선언할거에요 -> 체력 포션 아이템인 HealthPotion을 +{ // 코드 블록을 시작할거에요 -> HealthPotion 범위를 - // PlayerInteraction ȣ Լ - public void Use(PlayerHealth target) - { - if (target == null) return; + [Header("--- 포션 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> --- 포션 설정 --- 을 + [SerializeField] private float healAmount = 30f; // 변수를 선언할거에요 -> 회복량(30)을 healAmount에 - target.Heal(healAmount); // ÷̾ ü ȸ - Debug.Log($"[Potion] ü {healAmount} ȸ!"); + // PlayerInteraction에서 호출할 함수 // 설명을 적을거에요 -> 상호작용 스크립트가 이걸 실행한다는 걸 + public void Use(PlayerHealth target) // 함수를 선언할거에요 -> 대상 플레이어를 회복시키는 Use를 + { // 코드 블록을 시작할거에요 -> Use 범위를 + if (target == null) return; // 조건이 맞으면 종료할거에요 -> 대상이 없으면 아무것도 안 함 - Destroy(gameObject); // - } -} \ No newline at end of file + target.Heal(healAmount); // 함수를 실행할거에요 -> 플레이어 체력을 healAmount 만큼 회복 + Debug.Log($"[Potion] 체력 {healAmount} 회복!"); // 로그를 찍을거에요 -> 회복 로그 출력 + + Destroy(gameObject); // 오브젝트를 삭제할거에요 -> 사용한 포션 아이템 제거 + } // 코드 블록을 끝낼거에요 -> Use를 + +} // 코드 블록을 끝낼거에요 -> HealthPotion을 \ No newline at end of file diff --git a/Assets/Scripts/Items/VFX/ItemHighlight.cs b/Assets/Scripts/Items/VFX/ItemHighlight.cs index b4bf942a..79576057 100644 --- a/Assets/Scripts/Items/VFX/ItemHighlight.cs +++ b/Assets/Scripts/Items/VFX/ItemHighlight.cs @@ -1,63 +1,65 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class ItemHighlight : MonoBehaviour -{ - [Header("--- 참조 ---")] - [SerializeField] private MeshRenderer weaponRenderer; +public class ItemHighlight : MonoBehaviour // 클래스를 선언할거에요 -> 아이템 하이라이트(발광 깜빡임) 담당 ItemHighlight를 +{ // 코드 블록을 시작할거에요 -> ItemHighlight 범위를 - [Header("--- 하이라이트 설정 ---")] - [ColorUsage(true, true)] - [SerializeField] private Color highlightColor = Color.yellow; - [SerializeField] private float blinkSpeed = 2f; - [SerializeField] private float minIntensity = 0.5f; - [SerializeField] private float maxIntensity = 3.0f; + [Header("--- 참조 ---")] // 인스펙터에 제목을 표시할거에요 -> 참조 섹션을 + [SerializeField] private MeshRenderer weaponRenderer; // 변수를 선언할거에요 -> 하이라이트 줄 렌더러를 weaponRenderer에 - private Material _material; - private static readonly int EmissionProperty = Shader.PropertyToID("_EmissionColor"); + [Header("--- 하이라이트 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> 하이라이트 설정 섹션을 + [ColorUsage(true, true)] // 속성을 붙일거에요 -> HDR 색상도 고를 수 있게 + [SerializeField] private Color highlightColor = Color.yellow; // 변수를 선언할거에요 -> 발광 색상을 노란색으로 + [SerializeField] private float blinkSpeed = 2f; // 변수를 선언할거에요 -> 깜빡이는 속도를 2로 + [SerializeField] private float minIntensity = 0.5f; // 변수를 선언할거에요 -> 최소 발광 세기를 0.5로 + [SerializeField] private float maxIntensity = 3.0f; // 변수를 선언할거에요 -> 최대 발광 세기를 3.0으로 - private void Awake() - { - if (weaponRenderer == null) weaponRenderer = GetComponentInChildren(); + private Material _material; // 변수를 선언할거에요 -> 렌더러의 머티리얼을 담을 _material을 + private static readonly int EmissionProperty = Shader.PropertyToID("_EmissionColor"); // 값을 캐싱할거에요 -> _EmissionColor 프로퍼티 ID를 - if (weaponRenderer != null) - { - _material = weaponRenderer.material; - _material.EnableKeyword("_EMISSION"); - } - } + private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + if (weaponRenderer == null) weaponRenderer = GetComponentInChildren(); // 조건이 맞으면 실행할거에요 -> 렌더러가 비어있으면 자식에서 자동으로 찾기 - // ⭐ [추가] 부모 오브젝트(손)가 바뀌었을 때 실행됩니다. - private void OnTransformParentChanged() - { - // 부모가 없다면 = 바닥에 떨어졌다면 - if (transform.parent == null) - { - this.enabled = true; // 스크립트를 다시 깨웁니다. - } - } + if (weaponRenderer != null) // 조건을 검사할거에요 -> 렌더러를 찾았는지 + { // 코드 블록을 시작할거에요 -> 렌더러가 있을 때 처리 + _material = weaponRenderer.material; // 머티리얼을 가져올거에요 -> 렌더러의 material을 _material에 + _material.EnableKeyword("_EMISSION"); // 키워드를 켤거에요 -> 발광(Emission) 기능을 활성화 + } // 코드 블록을 끝낼거에요 -> 렌더러 처리 + } // 코드 블록을 끝낼거에요 -> Awake를 - private void Update() - { - if (_material == null) return; + // ⭐ [추가] 부모 오브젝트(손)가 바뀌었을 때 실행됩니다. // 설명을 적을거에요 -> 부모 변경 이벤트 훅임을 + private void OnTransformParentChanged() // 함수를 선언할거에요 -> 부모가 바뀌면 자동 호출되는 OnTransformParentChanged를 + { // 코드 블록을 시작할거에요 -> OnTransformParentChanged 범위를 + // 부모가 없다면 = 바닥에 떨어졌다면 // 설명을 적을거에요 -> 다시 주워지지 않은 상태라는 뜻 + if (transform.parent == null) // 조건을 검사할거에요 -> 부모가 없는지 + { // 코드 블록을 시작할거에요 -> 바닥에 떨어진 경우 처리 + this.enabled = true; // 스크립트를 켤거에요 -> Update가 다시 돌게 + } // 코드 블록을 끝낼거에요 -> 바닥 처리 + } // 코드 블록을 끝낼거에요 -> OnTransformParentChanged를 - // 누군가 주운 상태(부모가 있음)라면 빛을 끄고 퇴근합니다. - if (transform.parent != null) - { - StopHighlight(); - return; - } + private void Update() // 함수를 선언할거에요 -> 매 프레임 실행되는 Update를 + { // 코드 블록을 시작할거에요 -> Update 범위를 + if (_material == null) return; // 조건이 맞으면 종료할거에요 -> 머티리얼이 없으면 처리 불가 - float lerpValue = Mathf.PingPong(Time.time * blinkSpeed, 1f); - float intensity = Mathf.Lerp(minIntensity, maxIntensity, lerpValue); - _material.SetColor(EmissionProperty, highlightColor * intensity); - } + // 누군가 주운 상태(부모가 있음)라면 빛을 끄고 퇴근합니다. // 설명을 적을거에요 -> 손에 들린 상태면 하이라이트 중단 + if (transform.parent != null) // 조건을 검사할거에요 -> 부모가 있는지(주워졌는지) + { // 코드 블록을 시작할거에요 -> 주워진 경우 처리 + StopHighlight(); // 함수를 실행할거에요 -> 하이라이트 끄기 + return; // 함수를 끝낼거에요 -> 아래 발광 계산은 안 하게 + } // 코드 블록을 끝낼거에요 -> 주워진 처리 - public void StopHighlight() - { - if (_material != null) - { - _material.SetColor(EmissionProperty, Color.black); - this.enabled = false; // Update를 멈춥니다. - } - } -} \ No newline at end of file + float lerpValue = Mathf.PingPong(Time.time * blinkSpeed, 1f); // 값을 계산할거에요 -> 0~1로 왕복하는 값(깜빡임용)을 + float intensity = Mathf.Lerp(minIntensity, maxIntensity, lerpValue); // 값을 계산할거에요 -> 최소~최대 사이 발광 세기를 + _material.SetColor(EmissionProperty, highlightColor * intensity); // 값을 설정할거에요 -> 발광 색상을 (색 * 세기)로 + } // 코드 블록을 끝낼거에요 -> Update를 + + public void StopHighlight() // 함수를 선언할거에요 -> 하이라이트를 끄는 StopHighlight를 + { // 코드 블록을 시작할거에요 -> StopHighlight 범위를 + if (_material != null) // 조건을 검사할거에요 -> 머티리얼이 있는지 + { // 코드 블록을 시작할거에요 -> 머티리얼이 있을 때 처리 + _material.SetColor(EmissionProperty, Color.black); // 값을 설정할거에요 -> 발광을 검정(=0)으로 만들어 끄기 + this.enabled = false; // 컴포넌트를 끌거에요 -> Update를 멈추기 + } // 코드 블록을 끝낼거에요 -> 머티리얼 처리 + } // 코드 블록을 끝낼거에요 -> StopHighlight를 + +} // 코드 블록을 끝낼거에요 -> ItemHighlight를 \ No newline at end of file diff --git a/Assets/Scripts/Obsession/BossPatternPhases.cs b/Assets/Scripts/Obsession/BossPatternPhases.cs index 3909c3da..05008c0f 100644 --- a/Assets/Scripts/Obsession/BossPatternPhases.cs +++ b/Assets/Scripts/Obsession/BossPatternPhases.cs @@ -1,123 +1,125 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class BossPatternPhases : MonoBehaviour -{ - // [Header]는 유니티 Inspector 창에서 폴더처럼 보이게 해줌 - [Header("🔧 패턴 설정")] - [SerializeField] private int totalPhases = 3; // 전체 패턴 구간 수 (Inspector에서 조절 가능) +public class BossPatternPhases : MonoBehaviour // 클래스를 선언할거에요 -> 보스 패턴 페이즈 진행을 관리하는 BossPatternPhases를 +{ // 코드 블록을 시작할거에요 -> BossPatternPhases 범위를 - [Header("💎 XP 설정")] - [SerializeField] private int phaseClearXP = 10; // 한 구간 통과 시 주는 XP - [SerializeField] private int fullClearBonus = 30; // 모든 구간 통과 시 보너스 XP - [SerializeField] private int hitPenalty = -5; // 피격 시 감점 XP (0으로 하면 감점 없음) + // [Header]는 유니티 Inspector 창에서 폴더처럼 보이게 해줌 // 설명을 적을거에요 -> 인스펙터 그룹 라벨용 + [Header("🔧 패턴 설정")] // 인스펙터에 제목을 표시할거에요 -> 패턴 설정을 + [SerializeField] private int totalPhases = 3; // 변수를 선언할거에요 -> 전체 페이즈 수를 3으로 - // [SerializeField]는 인스펙터에서 실시간으로 볼 수 있게 해줌 - [Header("🚩 상태 확인용")] - [SerializeField] private int currentPhase = 0; // 현재 진행 중인 구간 (0: 시작 전) - [SerializeField] private bool isPatternActive = false; // 패턴이 진행 중인지 - [SerializeField] private bool wasHitThisPhase = false; // 이번 구간 중 피격당했는지 + [Header("💎 XP 설정")] // 인스펙터에 제목을 표시할거에요 -> XP 설정을 + [SerializeField] private int phaseClearXP = 10; // 변수를 선언할거에요 -> 페이즈 성공 시 XP를 10으로 + [SerializeField] private int fullClearBonus = 30; // 변수를 선언할거에요 -> 전체 클리어 보너스를 30으로 + [SerializeField] private int hitPenalty = -5; // 변수를 선언할거에요 -> 피격 시 감점 XP를 -5로 - // ★ 초기화 - private void Start() - { - ResetPattern(); - } + // [SerializeField]는 인스펙터에서 실시간으로 볼 수 있게 해줌 // 설명을 적을거에요 -> 런타임에도 값 확인 가능 + [Header("🚩 상태 확인용")] // 인스펙터에 제목을 표시할거에요 -> 상태 확인용을 + [SerializeField] private int currentPhase = 0; // 변수를 선언할거에요 -> 현재 페이즈를 0으로(시작 전) + [SerializeField] private bool isPatternActive = false; // 변수를 선언할거에요 -> 패턴 진행 여부를 false로 + [SerializeField] private bool wasHitThisPhase = false; // 변수를 선언할거에요 -> 이번 페이즈 피격 여부를 false로 - // ★ 패턴 전체 리셋 (새로운 패턴 시작 시 호출) - public void ResetPattern() - { - currentPhase = 0; - wasHitThisPhase = false; - isPatternActive = false; - Debug.Log("[보스] 패턴 초기화 완료!"); - } + // ★ 초기화 // 설명을 적을거에요 -> 시작 시 초기화 수행 + private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를 + { // 코드 블록을 시작할거에요 -> Start 범위를 + ResetPattern(); // 함수를 실행할거에요 -> 패턴 상태를 초기화 + } // 코드 블록을 끝낼거에요 -> Start를 - // ★ 패턴 시작 (보스 AI에서 호출) - public void StartPattern() - { - if (currentPhase >= totalPhases) - { - Debug.Log("이미 모든 패턴을 완료했어요!"); - return; - } + // ★ 패턴 전체 리셋 (새로운 패턴 시작 시 호출) // 설명을 적을거에요 -> 상태를 처음으로 되돌리는 함수 + public void ResetPattern() // 함수를 선언할거에요 -> 패턴 전체 초기화 ResetPattern을 + { // 코드 블록을 시작할거에요 -> ResetPattern 범위를 + currentPhase = 0; // 값을 설정할거에요 -> 현재 페이즈를 0으로 + wasHitThisPhase = false; // 값을 설정할거에요 -> 피격 여부를 false로 + isPatternActive = false; // 값을 설정할거에요 -> 패턴 진행 상태를 false로 + Debug.Log("[보스] 패턴 초기화 완료!"); // 로그를 찍을거에요 -> 초기화 완료 확인 + } // 코드 블록을 끝낼거에요 -> ResetPattern을 - isPatternActive = true; - wasHitThisPhase = false; - Debug.Log($"🔔 [페이즈 {currentPhase + 1}] 시작! 회피하세요!"); + // ★ 패턴 시작 (보스 AI에서 호출) // 설명을 적을거에요 -> 보스가 패턴을 시작할 때 호출 + public void StartPattern() // 함수를 선언할거에요 -> 패턴 시작 함수 StartPattern을 + { // 코드 블록을 시작할거에요 -> StartPattern 범위를 + if (currentPhase >= totalPhases) // 조건을 검사할거에요 -> 이미 전부 끝났는지 + { // 코드 블록을 시작할거에요 -> 전부 끝난 경우 처리 + Debug.Log("이미 모든 패턴을 완료했어요!"); // 로그를 찍을거에요 -> 더 이상 시작 불가 안내 + return; // 함수를 끝낼거에요 -> 아래 로직 실행 안 하게 + } // 코드 블록을 끝낼거에요 -> 완료 처리 - // ★ ★ ★ 여기서 보스의 애니메이션/이펙트 시작하면 됨 ★ ★ ★ - } + isPatternActive = true; // 값을 설정할거에요 -> 패턴 진행 상태를 true로 + wasHitThisPhase = false; // 값을 설정할거에요 -> 이번 페이즈 피격 여부 초기화 + Debug.Log($"🔔 [페이즈 {currentPhase + 1}] 시작! 회피하세요!"); // 로그를 찍을거에요 -> 페이즈 시작 알림 - // ★ 플레이어가 피격당했을 때 호출 (PlayerHealth에서 연결) - public void OnPlayerHit() - { - if (!isPatternActive) return; + // ★ ★ ★ 여기서 보스의 애니메이션/이펙트 시작하면 됨 ★ ★ ★ // 설명을 적을거에요 -> 연출 시작 지점 + } // 코드 블록을 끝낼거에요 -> StartPattern을 - wasHitThisPhase = true; - Debug.Log($"💥 [페이즈 {currentPhase + 1}] 중 피격당함! (XP 감점)"); + // ★ 플레이어가 피격당했을 때 호출 (PlayerHealth에서 연결) // 설명을 적을거에요 -> 플레이어 피격 이벤트 연결용 + public void OnPlayerHit() // 함수를 선언할거에요 -> 피격 처리 함수 OnPlayerHit을 + { // 코드 블록을 시작할거에요 -> OnPlayerHit 범위를 + if (!isPatternActive) return; // 조건이 맞으면 종료할거에요 -> 패턴 중이 아니면 무시 - // 피격 시 감점 처리 (필요하면) - if (hitPenalty != 0 && ObsessionSystem.instance != null) - { - ObsessionSystem.instance.AddRunXP(hitPenalty); - } - } + wasHitThisPhase = true; // 값을 설정할거에요 -> 이번 페이즈에서 맞았다고 표시 + Debug.Log($"💥 [페이즈 {currentPhase + 1}] 중 피격당함! (XP 감점)"); // 로그를 찍을거에요 -> 피격 안내 - // ★ 현재 구간을 성공적으로 통과했을 때 호출 (보스 AI에서 호출) - public void ClearCurrentPhase() - { - if (!isPatternActive) return; + // 피격 시 감점 처리 (필요하면) // 설명을 적을거에요 -> hitPenalty가 0이 아니면 XP 감소 + if (hitPenalty != 0 && ObsessionSystem.instance != null) // 조건을 검사할거에요 -> 감점이 있고 시스템이 존재하는지 + { // 코드 블록을 시작할거에요 -> 감점 처리 + ObsessionSystem.instance.AddRunXP(hitPenalty); // XP를 더할거에요 -> 음수면 감점됨 + } // 코드 블록을 끝낼거에요 -> 감점 처리 + } // 코드 블록을 끝낼거에요 -> OnPlayerHit을 - // 피격당했는지 확인 - if (wasHitThisPhase) - { - Debug.Log($"❌ [페이즈 {currentPhase + 1}] 실패... 다음 구간 진행"); - } - else - { - // ★ 성공 시 XP 지급 - if (ObsessionSystem.instance != null) - { - ObsessionSystem.instance.AddRunXP(phaseClearXP); - Debug.Log($"✅ [페이즈 {currentPhase + 1}] 성공! +{phaseClearXP}XP"); - } - } + // ★ 현재 구간을 성공적으로 통과했을 때 호출 (보스 AI에서 호출) // 설명을 적을거에요 -> 페이즈 종료/판정 처리 + public void ClearCurrentPhase() // 함수를 선언할거에요 -> 현재 페이즈 클리어 처리 ClearCurrentPhase를 + { // 코드 블록을 시작할거에요 -> ClearCurrentPhase 범위를 + if (!isPatternActive) return; // 조건이 맞으면 종료할거에요 -> 패턴 중이 아니면 무시 - // 다음 구간으로 - currentPhase++; - wasHitThisPhase = false; + // 피격당했는지 확인 // 설명을 적을거에요 -> 성공/실패 판정 기준 + if (wasHitThisPhase) // 조건을 검사할거에요 -> 이번 페이즈 중 피격 여부 + { // 코드 블록을 시작할거에요 -> 실패 처리 + Debug.Log($"❌ [페이즈 {currentPhase + 1}] 실패... 다음 구간 진행"); // 로그를 찍을거에요 -> 실패 안내 + } // 코드 블록을 끝낼거에요 -> 실패 처리 + else // 조건이 아니면 실행할거에요 -> 안 맞았으면 + { // 코드 블록을 시작할거에요 -> 성공 처리 + // ★ 성공 시 XP 지급 // 설명을 적을거에요 -> 시스템이 있으면 XP 지급 + if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> XP 시스템이 존재하는지 + { // 코드 블록을 시작할거에요 -> XP 지급 + ObsessionSystem.instance.AddRunXP(phaseClearXP); // XP를 더할거에요 -> phaseClearXP만큼 + Debug.Log($"✅ [페이즈 {currentPhase + 1}] 성공! +{phaseClearXP}XP"); // 로그를 찍을거에요 -> 성공 안내 + } // 코드 블록을 끝낼거에요 -> XP 지급 + } // 코드 블록을 끝낼거에요 -> 성공/실패 분기 - // 모든 구간을 완료했는가? - if (currentPhase >= totalPhases) - { - Debug.Log("🎉 모든 패턴 완료!"); - FullClearBonus(); - isPatternActive = false; - } - } + // 다음 구간으로 // 설명을 적을거에요 -> 페이즈 진행 업데이트 + currentPhase++; // 값을 증가시킬거에요 -> 다음 페이즈로 이동 + wasHitThisPhase = false; // 값을 초기화할거에요 -> 다음 페이즈를 위해 피격 여부 리셋 - // ★ 모든 구간 통과 시 보너스 - private void FullClearBonus() - { - if (ObsessionSystem.instance != null) - { - ObsessionSystem.instance.AddRunXP(fullClearBonus); - Debug.Log($"🌟 전체 패턴 클리어 보너스 +{fullClearBonus}XP!"); - } - } + // 모든 구간을 완료했는가? // 설명을 적을거에요 -> 전체 클리어 체크 + if (currentPhase >= totalPhases) // 조건을 검사할거에요 -> 마지막 페이즈까지 끝났는지 + { // 코드 블록을 시작할거에요 -> 전체 클리어 처리 + Debug.Log("🎉 모든 패턴 완료!"); // 로그를 찍을거에요 -> 전체 완료 안내 + FullClearBonus(); // 함수를 실행할거에요 -> 보너스 XP 지급 + isPatternActive = false; // 값을 설정할거에요 -> 패턴 종료 상태로 + } // 코드 블록을 끝낼거에요 -> 전체 클리어 처리 + } // 코드 블록을 끝낼거에요 -> ClearCurrentPhase를 - // ★ 디버그용: 현재 상태를 콘솔에 출력 - private void Update() - { - if (Input.GetKeyDown(KeyCode.P)) - { - Debug.Log($"[패턴 상태] 현재 구간: {currentPhase + 1}/{totalPhases}, 활성: {isPatternActive}"); - } - } + // ★ 모든 구간 통과 시 보너스 // 설명을 적을거에요 -> 전체 클리어 보너스 지급 + private void FullClearBonus() // 함수를 선언할거에요 -> 전체 클리어 보너스 FullClearBonus를 + { // 코드 블록을 시작할거에요 -> FullClearBonus 범위를 + if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> XP 시스템이 존재하는지 + { // 코드 블록을 시작할거에요 -> 보너스 지급 + ObsessionSystem.instance.AddRunXP(fullClearBonus); // XP를 더할거에요 -> 보너스 XP 지급 + Debug.Log($"🌟 전체 패턴 클리어 보너스 +{fullClearBonus}XP!"); // 로그를 찍을거에요 -> 보너스 안내 + } // 코드 블록을 끝낼거에요 -> 보너스 지급 + } // 코드 블록을 끝낼거에요 -> FullClearBonus를 - // BossPatternPhases.cs에 추가 - public bool IsPatternActive() - { - return isPatternActive; - } -} \ No newline at end of file + // ★ 디버그용: 현재 상태를 콘솔에 출력 // 설명을 적을거에요 -> 테스트용 단축키 출력 + private void Update() // 함수를 선언할거에요 -> 매 프레임 호출되는 Update를 + { // 코드 블록을 시작할거에요 -> Update 범위를 + if (Input.GetKeyDown(KeyCode.P)) // 조건을 검사할거에요 -> P 키를 눌렀는지 + { // 코드 블록을 시작할거에요 -> 디버그 출력 + Debug.Log($"[패턴 상태] 현재 구간: {currentPhase + 1}/{totalPhases}, 활성: {isPatternActive}"); // 로그를 찍을거에요 -> 현재 상태를 + } // 코드 블록을 끝낼거에요 -> 디버그 출력 + } // 코드 블록을 끝낼거에요 -> Update를 + + // BossPatternPhases.cs에 추가 // 설명을 적을거에요 -> 외부에서 패턴 진행 여부 확인용 + public bool IsPatternActive() // 함수를 선언할거에요 -> 패턴 활성 여부 반환 함수 IsPatternActive를 + { // 코드 블록을 시작할거에요 -> IsPatternActive 범위를 + return isPatternActive; // 값을 돌려줄거에요 -> 현재 활성 상태를 + } // 코드 블록을 끝낼거에요 -> IsPatternActive를 + +} // 코드 블록을 끝낼거에요 -> BossPatternPhases를 \ No newline at end of file diff --git a/Assets/Scripts/Obsession/BossRoomTrigger.cs b/Assets/Scripts/Obsession/BossRoomTrigger.cs index 79c81aaa..235db622 100644 --- a/Assets/Scripts/Obsession/BossRoomTrigger.cs +++ b/Assets/Scripts/Obsession/BossRoomTrigger.cs @@ -1,19 +1,22 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class BossRoomTrigger : MonoBehaviour -{ - private void OnTriggerEnter(Collider other) - { - if (other.CompareTag("Player")) - { - // ★ 보스 방에 처음 들어가면 보너스 XP - if (ObsessionSystem.instance != null) - { - ObsessionSystem.instance.AddRunXP(50); // 보스 도달 보너스 - Debug.Log("보스 방 도달! +50XP 보너스 획득!"); - } +public class BossRoomTrigger : MonoBehaviour // 클래스를 선언할거에요 -> 보스방 진입 트리거를 처리하는 BossRoomTrigger를 +{ // 코드 블록을 시작할거에요 -> BossRoomTrigger 범위를 - Destroy(this); // 한 번만 주고 제거 - } - } -} \ No newline at end of file + private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 뭔가 들어오면 호출되는 OnTriggerEnter를 + { // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를 + if (other.CompareTag("Player")) // 조건을 검사할거에요 -> 들어온 대상이 Player인지 + { // 코드 블록을 시작할거에요 -> 플레이어일 때 처리 + + // ★ 보스 방에 처음 들어가면 보너스 XP // 설명을 적을거에요 -> 1회만 보너스 지급 목적 + if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> XP 시스템 싱글톤이 존재하는지 + { // 코드 블록을 시작할거에요 -> XP 지급 처리 + ObsessionSystem.instance.AddRunXP(50); // XP를 더할거에요 -> 보스 도달 보너스 50을 + Debug.Log("보스 방 도달! +50XP 보너스 획득!"); // 로그를 찍을거에요 -> 보너스 지급 확인 + } // 코드 블록을 끝낼거에요 -> XP 지급 처리 + + Destroy(this); // 컴포넌트를 삭제할거에요 -> 한 번만 발동하게 BossRoomTrigger를 + } // 코드 블록을 끝낼거에요 -> 플레이어 처리 + } // 코드 블록을 끝낼거에요 -> OnTriggerEnter를 + +} // 코드 블록을 끝낼거에요 -> BossRoomTrigger를 \ No newline at end of file diff --git a/Assets/Scripts/Obsession/ObsessionSystem.cs b/Assets/Scripts/Obsession/ObsessionSystem.cs index 6afe7c67..c83a7070 100644 --- a/Assets/Scripts/Obsession/ObsessionSystem.cs +++ b/Assets/Scripts/Obsession/ObsessionSystem.cs @@ -1,118 +1,110 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class ObsessionSystem : MonoBehaviour -{ - public static ObsessionSystem instance; +public class ObsessionSystem : MonoBehaviour // 클래스를 선언할거에요 -> 영구 XP/런 XP/레벨을 관리하는 ObsessionSystem을 +{ // 코드 블록을 시작할거에요 -> ObsessionSystem 범위를 - [SerializeField] private int currentXP = 0; // ★ 영구 XP (저장되는 진짜 진행도) - [SerializeField] private int currentLevel = 1; - [SerializeField] private int[] xpRequirements = { 100, 300, 500, 800, 1200 }; + public static ObsessionSystem instance; // 정적 변수를 선언할거에요 -> 어디서든 접근 가능한 싱글톤 instance를 - // ★ ★ ★ NEW: 이번 런에서 쌓은 임시 XP ★ ★ ★ - [SerializeField] private int currentRunXP = 0; // 이 값은 게임 껐다 키면 사라져요! + [SerializeField] private int currentXP = 0; // 변수를 선언할거에요 -> 영구 XP(저장되는 진행도)를 currentXP에 + [SerializeField] private int currentLevel = 1; // 변수를 선언할거에요 -> 현재 레벨을 currentLevel에 + [SerializeField] private int[] xpRequirements = { 100, 300, 500, 800, 1200 }; // 배열을 선언할거에요 -> 레벨업 필요 XP 목록을 - private void Awake() - { - if (instance == null) - { - instance = this; - DontDestroyOnLoad(gameObject); - } - else - { - Destroy(gameObject); - } - } + [SerializeField] private int currentRunXP = 0; // 변수를 선언할거에요 -> 이번 런에서 쌓인 임시 XP를 currentRunXP에 - private void Start() - { - LoadData(); - Debug.Log($"[집착 시스템] 시작! 현재 레벨: {currentLevel}, XP: {currentXP}"); + private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + if (instance == null) // 조건을 검사할거에요 -> 싱글톤이 아직 없으면 + { // 코드 블록을 시작할거에요 -> 최초 생성 처리 + instance = this; // 값을 넣을거에요 -> instance에 자신을 등록 + DontDestroyOnLoad(gameObject); // 파괴하지 않을거에요 -> 씬이 바뀌어도 유지되게 + } // 코드 블록을 끝낼거에요 -> 최초 생성 처리 + else // 조건이 아니면 실행할거에요 -> 이미 instance가 있으면 + { // 코드 블록을 시작할거에요 -> 중복 제거 처리 + Destroy(gameObject); // 파괴할거에요 -> 중복 오브젝트를 + } // 코드 블록을 끝낼거에요 -> 중복 제거 처리 + } // 코드 블록을 끝낼거에요 -> Awake를 - // ★ ★ ★ NEW: 새 런 시작할 때 임시 XP는 0으로 초기화 ★ ★ ★ - currentRunXP = 0; - Debug.Log($"[새 런 시작] 이번 런에 쌓을 임시 XP: 0"); - } + private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를 + { // 코드 블록을 시작할거에요 -> Start 범위를 + LoadData(); // 함수를 실행할거에요 -> 저장된 데이터 불러오기 + Debug.Log($"[집착 시스템] 시작! 현재 레벨: {currentLevel}, XP: {currentXP}"); // 로그를 찍을거에요 -> 시작 상태 출력 - // ★ ★ ★ NEW: 플레이 중 실시간으로 임시 XP 추가 ★ ★ ★ - public void AddRunXP(int amount) - { - currentRunXP += amount; - Debug.Log($"[런 XP 획득] +{amount}XP (이번 런 총: {currentRunXP})"); - } + currentRunXP = 0; // 값을 초기화할거에요 -> 새 런 시작이니 임시 XP를 0으로 + Debug.Log($"[새 런 시작] 이번 런에 쌓을 임시 XP: 0"); // 로그를 찍을거에요 -> 런 XP 초기화 확인 + } // 코드 블록을 끝낼거에요 -> Start를 - // ★ ★ ★ NEW: 사망 시 임시 XP를 영구 XP로 변환 ★ ★ ★ - public void OnDeathConvertXP() - { - Debug.Log($"[사망] 이번 런 성과: {currentRunXP}XP를 영구 XP로 전환!"); + public void AddRunXP(int amount) // 함수를 선언할거에요 -> 플레이 중 임시 XP를 더하는 AddRunXP를 + { // 코드 블록을 시작할거에요 -> AddRunXP 범위를 + currentRunXP += amount; // 값을 더할거에요 -> 임시 XP에 amount만큼 + Debug.Log($"[런 XP 획득] +{amount}XP (이번 런 총: {currentRunXP})"); // 로그를 찍을거에요 -> 현재 런 XP 출력 + } // 코드 블록을 끝낼거에요 -> AddRunXP를 - // 임시 XP를 영구 XP에 추가 - AddXP(currentRunXP); + public void OnDeathConvertXP() // 함수를 선언할거에요 -> 사망 시 임시 XP를 영구 XP로 바꾸는 OnDeathConvertXP를 + { // 코드 블록을 시작할거에요 -> OnDeathConvertXP 범위를 + Debug.Log($"[사망] 이번 런 성과: {currentRunXP}XP를 영구 XP로 전환!"); // 로그를 찍을거에요 -> 전환 안내 - // 임시 XP는 초기화 (다음 런을 위해) - currentRunXP = 0; - } + AddXP(currentRunXP); // 함수를 실행할거에요 -> 임시 XP만큼 영구 XP에 추가(레벨업/저장 포함) - // 기존 AddXP 함수 (이제는 사망할 때만 호출될 거예요) - public void AddXP(int amount) - { - currentXP += amount; - Debug.Log($"[영구 XP 획득] +{amount}XP (현재: {currentXP})"); - CheckLevelUp(); - SaveData(); - } + currentRunXP = 0; // 값을 초기화할거에요 -> 다음 런을 위해 임시 XP를 0으로 + } // 코드 블록을 끝낼거에요 -> OnDeathConvertXP를 - private void CheckLevelUp() - { - if (currentLevel - 1 < xpRequirements.Length) - { - int requiredXP = xpRequirements[currentLevel - 1]; - if (currentXP >= requiredXP) - { - currentLevel++; - Debug.Log($"🎉 [레벨업!] 레벨 {currentLevel - 1} → {currentLevel}"); - UnlockRewards(currentLevel); - } - } - } + public void AddXP(int amount) // 함수를 선언할거에요 -> 영구 XP를 더하는 AddXP를 + { // 코드 블록을 시작할거에요 -> AddXP 범위를 + currentXP += amount; // 값을 더할거에요 -> 영구 XP에 amount만큼 + Debug.Log($"[영구 XP 획득] +{amount}XP (현재: {currentXP})"); // 로그를 찍을거에요 -> 영구 XP 현황 출력 + CheckLevelUp(); // 함수를 실행할거에요 -> 레벨업 조건 확인 + SaveData(); // 함수를 실행할거에요 -> 변경된 데이터 저장 + } // 코드 블록을 끝낼거에요 -> AddXP를 - private void UnlockRewards(int level) - { - // 보상 코드는 기존과 동일 - } + private void CheckLevelUp() // 함수를 선언할거에요 -> 레벨업 여부를 검사하는 CheckLevelUp을 + { // 코드 블록을 시작할거에요 -> CheckLevelUp 범위를 + if (currentLevel - 1 < xpRequirements.Length) // 조건을 검사할거에요 -> 요구치 배열 범위 안인지 + { // 코드 블록을 시작할거에요 -> 범위 안이면 + int requiredXP = xpRequirements[currentLevel - 1]; // 값을 가져올거에요 -> 현재 레벨의 필요 XP를 requiredXP에 + if (currentXP >= requiredXP) // 조건을 검사할거에요 -> 영구 XP가 요구치 이상인지 + { // 코드 블록을 시작할거에요 -> 레벨업 처리 + currentLevel++; // 레벨을 올릴거에요 -> 현재 레벨 +1 + Debug.Log($"🎉 [레벨업!] 레벨 {currentLevel - 1} → {currentLevel}"); // 로그를 찍을거에요 -> 레벨업 안내 + UnlockRewards(currentLevel); // 함수를 실행할거에요 -> 레벨 보상 해금 처리 + } // 코드 블록을 끝낼거에요 -> 레벨업 처리 + } // 코드 블록을 끝낼거에요 -> 범위 체크 + } // 코드 블록을 끝낼거에요 -> CheckLevelUp을 - private void SaveData() - { - PlayerPrefs.SetInt("ObsessionLevel", currentLevel); - PlayerPrefs.SetInt("ObsessionXP", currentXP); - PlayerPrefs.Save(); - } + private void UnlockRewards(int level) // 함수를 선언할거에요 -> 레벨 보상을 해금하는 UnlockRewards를 + { // 코드 블록을 시작할거에요 -> UnlockRewards 범위를 + // 보상 코드는 기존과 동일 // 설명을 적을거에요 -> 보상 구현은 여기에 추가하면 됨 + } // 코드 블록을 끝낼거에요 -> UnlockRewards를 - private void LoadData() - { - currentLevel = PlayerPrefs.GetInt("ObsessionLevel", 1); - currentXP = PlayerPrefs.GetInt("ObsessionXP", 0); - } + private void SaveData() // 함수를 선언할거에요 -> PlayerPrefs에 저장하는 SaveData를 + { // 코드 블록을 시작할거에요 -> SaveData 범위를 + PlayerPrefs.SetInt("ObsessionLevel", currentLevel); // 값을 저장할거에요 -> 레벨을 ObsessionLevel 키로 + PlayerPrefs.SetInt("ObsessionXP", currentXP); // 값을 저장할거에요 -> 영구 XP를 ObsessionXP 키로 + PlayerPrefs.Save(); // 저장을 확정할거에요 -> PlayerPrefs.Save 호출 + } // 코드 블록을 끝낼거에요 -> SaveData를 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // 🧪 테스트용: 현재 상태를 읽는 함수들 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + private void LoadData() // 함수를 선언할거에요 -> PlayerPrefs에서 불러오는 LoadData를 + { // 코드 블록을 시작할거에요 -> LoadData 범위를 + currentLevel = PlayerPrefs.GetInt("ObsessionLevel", 1); // 값을 불러올거에요 -> 저장된 레벨이 없으면 1로 + currentXP = PlayerPrefs.GetInt("ObsessionXP", 0); // 값을 불러올거에요 -> 저장된 XP가 없으면 0으로 + } // 코드 블록을 끝낼거에요 -> LoadData를 - // 현재 런 XP 확인 - public int GetCurrentRunXP() - { - return currentRunXP; - } + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 구분선을 적을거에요 -> 테스트/조회 섹션 시작을 + // 🧪 테스트용: 현재 상태를 읽는 함수들 // 설명을 적을거에요 -> 외부 조회용 getter들 + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 구분선을 적을거에요 -> 테스트/조회 섹션을 - // 현재 영구 XP 확인 - public int GetCurrentXP() - { - return currentXP; - } + public int GetCurrentRunXP() // 함수를 선언할거에요 -> 현재 런 XP를 반환하는 GetCurrentRunXP를 + { // 코드 블록을 시작할거에요 -> GetCurrentRunXP 범위를 + return currentRunXP; // 값을 돌려줄거에요 -> 임시 XP를 + } // 코드 블록을 끝낼거에요 -> GetCurrentRunXP를 - // 현재 레벨 확인 - public int GetCurrentLevel() - { - return currentLevel; - } -} \ No newline at end of file + public int GetCurrentXP() // 함수를 선언할거에요 -> 현재 영구 XP를 반환하는 GetCurrentXP를 + { // 코드 블록을 시작할거에요 -> GetCurrentXP 범위를 + return currentXP; // 값을 돌려줄거에요 -> 영구 XP를 + } // 코드 블록을 끝낼거에요 -> GetCurrentXP를 + + public int GetCurrentLevel() // 함수를 선언할거에요 -> 현재 레벨을 반환하는 GetCurrentLevel을 + { // 코드 블록을 시작할거에요 -> GetCurrentLevel 범위를 + return currentLevel; // 값을 돌려줄거에요 -> 현재 레벨을 + } // 코드 블록을 끝낼거에요 -> GetCurrentLevel을 + +} // 코드 블록을 끝낼거에요 -> ObsessionSystem을 \ No newline at end of file diff --git a/Assets/Scripts/Obsession/RoomClear.cs b/Assets/Scripts/Obsession/RoomClear.cs index 9b6ba5c8..561600cc 100644 --- a/Assets/Scripts/Obsession/RoomClear.cs +++ b/Assets/Scripts/Obsession/RoomClear.cs @@ -1,14 +1,18 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class RoomClear : MonoBehaviour -{ - public void ClearRoom() - { - // ★ 방을 무사히 클리어하면 - if (ObsessionSystem.instance != null) - { - ObsessionSystem.instance.AddRunXP(20); - Debug.Log("방 클리어! +20XP"); - } - } -} \ No newline at end of file +public class RoomClear : MonoBehaviour // 클래스를 선언할거에요 -> 방 클리어 보상을 주는 RoomClear를 +{ // 코드 블록을 시작할거에요 -> RoomClear 범위를 + + public void ClearRoom() // 함수를 선언할거에요 -> 방을 클리어했을 때 호출할 ClearRoom을 + { // 코드 블록을 시작할거에요 -> ClearRoom 범위를 + + // ★ 방을 무사히 클리어하면 // 설명을 적을거에요 -> 클리어 보상 지급 구간 + if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> 집착 시스템이 존재하는지 + { // 코드 블록을 시작할거에요 -> XP 지급 처리 + ObsessionSystem.instance.AddRunXP(20); // 함수를 실행할거에요 -> 이번 런 XP에 20 추가 + Debug.Log("방 클리어! +20XP"); // 로그를 찍을거에요 -> 보상 지급 확인 + } // 코드 블록을 끝낼거에요 -> XP 지급 처리 + + } // 코드 블록을 끝낼거에요 -> ClearRoom을 + +} // 코드 블록을 끝낼거에요 -> RoomClear를 \ No newline at end of file diff --git a/Assets/Scripts/Player/Animation.cs b/Assets/Scripts/Player/Animation.cs index 5e9d57cd..a3a249ec 100644 --- a/Assets/Scripts/Player/Animation.cs +++ b/Assets/Scripts/Player/Animation.cs @@ -1,19 +1,19 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class PlayerAnimator : MonoBehaviour +public class PlayerAnimator : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerAnimator를 { - [SerializeField] private Animator anim; - [SerializeField] private PlayerHealth health; + [SerializeField] private Animator anim; // 변수를 선언할거에요 -> 애니메이터 컴포넌트 anim을 + [SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 플레이어 체력 스크립트 health를 // ⭐ 추가: 진짜 공격 로직이 있는 스크립트 연결 - [SerializeField] private PlayerAttack attack; + [SerializeField] private PlayerAttack attack; // 변수를 선언할거에요 -> 플레이어 공격 스크립트 attack을 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를 { - if (health != null) + if (health != null) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 { - health.OnHitEvent += () => anim.SetTrigger("Hit"); - health.OnDead += () => anim.SetBool("Die", true); + health.OnHitEvent += () => anim.SetTrigger("Hit"); // 이벤트를 구독할거에요 -> 피격 시 Hit 트리거 발동을 + health.OnDead += () => anim.SetBool("Die", true); // 이벤트를 구독할거에요 -> 사망 시 Die 상태 변경을 } } @@ -21,19 +21,19 @@ public class PlayerAnimator : MonoBehaviour // ⭐ [애니메이션 이벤트 전달자] // 애니메이션 창에 적은 이름과 똑같은 함수를 만듭니다. // --------------------------------------------------------- - public void StartWeaponCollision() + public void StartWeaponCollision() // 함수를 선언할거에요 -> 무기 충돌 시작 이벤트 StartWeaponCollision을 { - if (attack != null) attack.StartWeaponCollision(); + if (attack != null) attack.StartWeaponCollision(); // 실행할거에요 -> 공격 스크립트의 충돌 시작 함수를 } - public void StopWeaponCollision() + public void StopWeaponCollision() // 함수를 선언할거에요 -> 무기 충돌 종료 이벤트 StopWeaponCollision을 { - if (attack != null) attack.StopWeaponCollision(); + if (attack != null) attack.StopWeaponCollision(); // 실행할거에요 -> 공격 스크립트의 충돌 종료 함수를 } // --------------------------------------------------------- - public void UpdateMove(float speedValue) => anim.SetFloat("Speed", speedValue); - // public void TriggerAttack() => anim.SetTrigger("Attack"); - public void TriggerThrow() => anim.SetTrigger("Throw"); - public void SetCharging(bool isCharging) => anim.SetBool("isCharging", isCharging); + public void UpdateMove(float speedValue) => anim.SetFloat("Speed", speedValue); // 값을 전달할거에요 -> 애니메이터의 Speed 파라미터에 + // public void TriggerAttack() => anim.SetTrigger("Attack"); + public void TriggerThrow() => anim.SetTrigger("Throw"); // 신호를 보낼거에요 -> 애니메이터의 Throw 트리거에 + public void SetCharging(bool isCharging) => anim.SetBool("isCharging", isCharging); // 값을 설정할거에요 -> 애니메이터의 isCharging 불리언에 } \ No newline at end of file diff --git a/Assets/Scripts/Player/Combat/Arrow.cs b/Assets/Scripts/Player/Combat/Arrow.cs index 73570cb1..4f7c9b50 100644 --- a/Assets/Scripts/Player/Combat/Arrow.cs +++ b/Assets/Scripts/Player/Combat/Arrow.cs @@ -1,57 +1,57 @@ -using UnityEngine; -using System.Collections; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 /// /// 발사된 화살 발사체 (파티클 프리팹에 부착됨) /// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지) /// [NEW] 속성 데미지(DoT) 시스템 추가 /// -public class PlayerArrow : MonoBehaviour +public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerArrow를 { - [Header("--- 정밀 피격 판정 설정 ---")] - [SerializeField] private float raycastDistance = 0.8f; - [SerializeField] private float raycastRadius = 0.08f; - [SerializeField] private LayerMask hitLayers; - [SerializeField] private Transform arrowTip; + [Header("--- 정밀 피격 판정 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 정밀 피격 판정 설정 --- 을 + [SerializeField] private float raycastDistance = 0.8f; // 변수를 선언할거에요 -> 레이캐스트 거리인 raycastDistance를 + [SerializeField] private float raycastRadius = 0.08f; // 변수를 선언할거에요 -> 레이캐스트 반지름인 raycastRadius를 + [SerializeField] private LayerMask hitLayers; // 변수를 선언할거에요 -> 충돌 레이어인 hitLayers를 + [SerializeField] private Transform arrowTip; // 변수를 선언할거에요 -> 화살 끝부분 위치인 arrowTip을 - [Header("--- 디버그 시각화 ---")] - [SerializeField] private bool showDebugRay = true; + [Header("--- 디버그 시각화 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 디버그 시각화 --- 를 + [SerializeField] private bool showDebugRay = true; // 변수를 선언할거에요 -> 디버그 레이 표시 여부인 showDebugRay를 // 화살 스탯 - private float damage; - private float speed; - private float range; - private Vector3 startPos; - private Vector3 shootDirection; - private bool isFired = false; - private bool hasHit = false; + private float damage; // 변수를 선언할거에요 -> 데미지 damage를 + private float speed; // 변수를 선언할거에요 -> 속도 speed를 + private float range; // 변수를 선언할거에요 -> 사거리 range를 + private Vector3 startPos; // 변수를 선언할거에요 -> 시작 위치 startPos를 + private Vector3 shootDirection; // 변수를 선언할거에요 -> 발사 방향 shootDirection을 + private bool isFired = false; // 변수를 초기화할거에요 -> 발사 여부 isFired를 거짓으로 + private bool hasHit = false; // 변수를 초기화할거에요 -> 충돌 여부 hasHit을 거짓으로 // [NEW] 속성 데미지 시스템 - private ArrowElementType elementType = ArrowElementType.None; - private float elementDamage = 0f; - private float elementDuration = 0f; + private ArrowElementType elementType = ArrowElementType.None; // 변수를 초기화할거에요 -> 속성 타입 elementType을 없음으로 + private float elementDamage = 0f; // 변수를 초기화할거에요 -> 속성 데미지 elementDamage를 0으로 + private float elementDuration = 0f; // 변수를 초기화할거에요 -> 속성 지속 시간 elementDuration을 0으로 // Raycast 추적용 - private Vector3 previousPosition; - private Rigidbody rb; + private Vector3 previousPosition; // 변수를 선언할거에요 -> 이전 위치 previousPosition을 + private Rigidbody rb; // 변수를 선언할거에요 -> 물리 컴포넌트 rb를 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를 { - rb = GetComponent(); + rb = GetComponent(); // 컴포넌트를 가져올거에요 -> 리지드바디를 } - private void Start() + private void Start() // 함수를 실행할거에요 -> 활성화 시 Start를 { - if (arrowTip == null) + if (arrowTip == null) // 조건이 맞으면 실행할거에요 -> 화살 팁이 설정되지 않았다면 { - Transform tip = transform.Find("ArrowTip"); - if (tip == null) tip = transform.Find("Tip"); - arrowTip = tip ?? transform; + Transform tip = transform.Find("ArrowTip"); // 찾을거에요 -> 자식 중 ArrowTip을 + if (tip == null) tip = transform.Find("Tip"); // 못 찾으면 찾을거에요 -> Tip을 + arrowTip = tip ?? transform; // 그래도 없으면 할당할거에요 -> 자기 자신의 위치를 } - if (hitLayers.value == 0) + if (hitLayers.value == 0) // 조건이 맞으면 실행할거에요 -> 충돌 레이어가 설정되지 않았다면 { - hitLayers = LayerMask.GetMask("Enemy", "EnemyHitBox", "Wall", "Ground", "Default"); + hitLayers = LayerMask.GetMask("Enemy", "EnemyHitBox", "Wall", "Ground", "Default"); // 값을 넣을거에요 -> 기본 충돌 레이어들을 } } @@ -66,83 +66,83 @@ public class PlayerArrow : MonoBehaviour Vector3 direction, ArrowElementType element, float elemDmg, - float elemDur) + float elemDur) // 함수를 선언할거에요 -> 화살을 초기화하는 Initialize를 { - this.damage = dmg; - this.speed = arrowSpeed; - this.range = maxRange; - this.shootDirection = direction.normalized; - this.startPos = transform.position; - this.previousPosition = transform.position; - this.isFired = true; + this.damage = dmg; // 값을 저장할거에요 -> 데미지를 + this.speed = arrowSpeed; // 값을 저장할거에요 -> 속도를 + this.range = maxRange; // 값을 저장할거에요 -> 사거리를 + this.shootDirection = direction.normalized; // 값을 저장할거에요 -> 정규화된 발사 방향을 + this.startPos = transform.position; // 값을 저장할거에요 -> 시작 위치를 + this.previousPosition = transform.position; // 값을 저장할거에요 -> 이전 위치를 (레이캐스트용) + this.isFired = true; // 상태를 바꿀거에요 -> 발사됨 상태로 // [NEW] 속성 정보 저장 - this.elementType = element; - this.elementDamage = elemDmg; - this.elementDuration = elemDur; + this.elementType = element; // 값을 저장할거에요 -> 속성 타입을 + this.elementDamage = elemDmg; // 값을 저장할거에요 -> 속성 데미지를 + this.elementDuration = elemDur; // 값을 저장할거에요 -> 속성 지속 시간을 // 발사 방향으로 회전 - if (shootDirection != Vector3.zero) + if (shootDirection != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 유효하다면 { - transform.rotation = Quaternion.LookRotation(shootDirection); + transform.rotation = Quaternion.LookRotation(shootDirection); // 회전시킬거에요 -> 발사 방향을 보도록 } // Rigidbody 설정 - if (rb == null) rb = GetComponent(); + if (rb == null) rb = GetComponent(); // 컴포넌트를 가져올거에요 -> 없다면 리지드바디를 - if (rb != null) + if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면 { - rb.useGravity = false; - rb.isKinematic = false; - rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; - rb.velocity = shootDirection * speed; + rb.useGravity = false; // 설정을 바꿀거에요 -> 중력을 끄기로 + rb.isKinematic = false; // 설정을 바꿀거에요 -> 물리 연산을 켜기로 + rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 설정을 바꿀거에요 -> 연속 충돌 감지로 + rb.velocity = shootDirection * speed; // 값을 넣을거에요 -> 방향과 속도를 곱해 물리 속도로 } - Destroy(gameObject, 5f); + Destroy(gameObject, 5f); // 파괴할거에요 -> 5초 뒤에 안전장치로 } /// /// [하위 호환성] 방향 없는 3-파라미터 버전 /// - public void Initialize(float dmg, float arrowSpeed, float maxRange) + public void Initialize(float dmg, float arrowSpeed, float maxRange) // 함수를 선언할거에요 -> 구버전 호환용 초기화 함수를 { Initialize(dmg, arrowSpeed, maxRange, transform.forward, - ArrowElementType.None, 0f, 0f); + ArrowElementType.None, 0f, 0f); // 실행할거에요 -> 신버전 초기화 함수를 기본값으로 } - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를 { - if (!isFired || hasHit) return; + if (!isFired || hasHit) return; // 조건이 맞으면 중단할거에요 -> 발사되지 않았거나 이미 맞았다면 // 사거리 체크 - float traveledDistance = Vector3.Distance(startPos, transform.position); - if (traveledDistance >= range) + float traveledDistance = Vector3.Distance(startPos, transform.position); // 거리를 계산할거에요 -> 이동 거리를 + if (traveledDistance >= range) // 조건이 맞으면 실행할거에요 -> 사거리를 넘었다면 { - Destroy(gameObject); - return; + Destroy(gameObject); // 파괴할거에요 -> 화살을 + return; // 중단할거에요 -> 함수를 } // 정밀 충돌 감지 - CheckPrecisionCollision(); + CheckPrecisionCollision(); // 함수를 실행할거에요 -> 정밀 충돌 감지 함수를 - previousPosition = transform.position; + previousPosition = transform.position; // 값을 저장할거에요 -> 현재 위치를 이전 위치로 } /// /// SphereCast 기반 정밀 충돌 (기존 로직 100% 유지) /// - private void CheckPrecisionCollision() + private void CheckPrecisionCollision() // 함수를 선언할거에요 -> 정밀 충돌을 체크하는 CheckPrecisionCollision을 { - Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; + Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치를 Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f ? rb.velocity.normalized - : transform.forward; + : transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면을 - float frameDistance = Vector3.Distance(previousPosition, transform.position); - float checkDistance = Mathf.Max(frameDistance, raycastDistance); + float frameDistance = Vector3.Distance(previousPosition, transform.position); // 거리를 계산할거에요 -> 프레임 간 이동 거리를 + float checkDistance = Mathf.Max(frameDistance, raycastDistance); // 값을 결정할거에요 -> 이동 거리와 최소 거리 중 큰 값을 - RaycastHit hit; + RaycastHit hit; // 변수를 선언할거에요 -> 충돌 정보를 담을 hit을 bool didHit = Physics.SphereCast( tipPosition, raycastRadius, @@ -150,64 +150,64 @@ public class PlayerArrow : MonoBehaviour out hit, checkDistance, hitLayers - ); + ); // 실행할거에요 -> 구체 캐스트를 쏴서 충돌 여부를 확인하는 것을 - if (showDebugRay) + if (showDebugRay) // 조건이 맞으면 실행할거에요 -> 디버그 모드라면 { - Debug.DrawRay(tipPosition, direction * checkDistance, didHit ? Color.red : Color.green, 0.1f); + Debug.DrawRay(tipPosition, direction * checkDistance, didHit ? Color.red : Color.green, 0.1f); // 선을 그릴거에요 -> 충돌 시 빨강, 아니면 초록으로 } - if (didHit) + if (didHit) // 조건이 맞으면 실행할거에요 -> 충돌했다면 { - HandleHit(hit.collider, hit.point); + HandleHit(hit.collider, hit.point); // 실행할거에요 -> 충돌 처리 함수 HandleHit을 } } /// /// [MODIFIED] 충돌 처리 — 속성 데미지 적용 추가 /// - private void HandleHit(Collider hitCollider, Vector3 hitPoint) + private void HandleHit(Collider hitCollider, Vector3 hitPoint) // 함수를 선언할거에요 -> 충돌 결과를 처리하는 HandleHit을 { - if (hasHit) return; - hasHit = true; + if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 충돌 처리되었다면 + hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로 // 적 감지 (Tag 또는 Layer) - bool isEnemy = hitCollider.CompareTag("Enemy"); + bool isEnemy = hitCollider.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지 // EnemyHitBox 레이어 체크 (레이어가 존재하는 경우에만) - int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); - if (!isEnemy && enemyHitBoxLayerIndex != -1) + int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 레이어 인덱스를 + if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면 { - isEnemy = hitCollider.gameObject.layer == enemyHitBoxLayerIndex; + isEnemy = hitCollider.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 해당 레이어인지 확인해서 } - if (isEnemy) + if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면 { // MonsterClass를 찾아 데미지 적용 - MonsterClass monster = hitCollider.GetComponentInParent(); - if (monster == null) monster = hitCollider.GetComponent(); + MonsterClass monster = hitCollider.GetComponentInParent(); // 컴포넌트를 찾을거에요 -> 부모의 몬스터 클래스를 + if (monster == null) monster = hitCollider.GetComponent(); // 없으면 찾을거에요 -> 자신의 몬스터 클래스를 - if (monster != null) + if (monster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 있다면 { // 1. 기본 데미지 적용 - monster.TakeDamage(damage); - Debug.Log($"적 명중! 기본데미지: {damage}"); + monster.TakeDamage(damage); // 실행할거에요 -> 데미지 입히기 함수를 + Debug.Log($"적 명중! 기본데미지: {damage}"); // 로그를 출력할거에요 -> 명중 메시지를 // 2. [NEW] 속성 효과 적용 - ApplyElementEffect(monster); + ApplyElementEffect(monster); // 실행할거에요 -> 속성 효과 적용 함수를 } - Destroy(gameObject, 0.05f); + Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 잠시 뒤 } - else if (hitCollider.CompareTag("Wall") || hitCollider.CompareTag("Ground")) + else if (hitCollider.CompareTag("Wall") || hitCollider.CompareTag("Ground")) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면 { - Debug.Log($"벽/바닥 충돌! 위치: {hitPoint}"); - Destroy(gameObject, 0.05f); + Debug.Log($"벽/바닥 충돌! 위치: {hitPoint}"); // 로그를 출력할거에요 -> 충돌 위치를 + Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 } - else + else // 조건이 틀리면 실행할거에요 -> 그 외의 충돌이라면 { // 기타 충돌 (Unknown) - Destroy(gameObject, 0.05f); + Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 } } @@ -215,31 +215,31 @@ public class PlayerArrow : MonoBehaviour /// [NEW] 속성 효과 적용 /// MonsterClass에 ApplyStatusEffect 메서드가 있어야 합니다. /// - private void ApplyElementEffect(MonsterClass monster) + private void ApplyElementEffect(MonsterClass monster) // 함수를 선언할거에요 -> 속성 효과를 적용하는 ApplyElementEffect를 { - if (elementType == ArrowElementType.None || elementDamage <= 0f) return; + if (elementType == ArrowElementType.None || elementDamage <= 0f) return; // 조건이 맞으면 중단할거에요 -> 속성이 없거나 데미지가 없다면 // MonsterClass에 ApplyStatusEffect가 있는지 확인 - switch (elementType) + switch (elementType) // 분기할거에요 -> 속성 타입에 따라 { - case ArrowElementType.Fire: - monster.ApplyStatusEffect(StatusEffectType.Burn, elementDamage, elementDuration); - Debug.Log($"화염 효과! {elementDamage} 데미지 x {elementDuration}초"); + case ArrowElementType.Fire: // 조건이 맞으면 실행할거에요 -> 불 속성이라면 + monster.ApplyStatusEffect(StatusEffectType.Burn, elementDamage, elementDuration); // 실행할거에요 -> 화상 효과 적용을 + Debug.Log($"화염 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을 break; - case ArrowElementType.Ice: - monster.ApplyStatusEffect(StatusEffectType.Slow, elementDamage, elementDuration); - Debug.Log($"빙결 효과! 슬로우 {elementDuration}초"); + case ArrowElementType.Ice: // 조건이 맞으면 실행할거에요 -> 얼음 속성이라면 + monster.ApplyStatusEffect(StatusEffectType.Slow, elementDamage, elementDuration); // 실행할거에요 -> 슬로우 효과 적용을 + Debug.Log($"빙결 효과! 슬로우 {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을 break; - case ArrowElementType.Poison: - monster.ApplyStatusEffect(StatusEffectType.Poison, elementDamage, elementDuration); - Debug.Log($"독 효과! {elementDamage} 데미지 x {elementDuration}초"); + case ArrowElementType.Poison: // 조건이 맞으면 실행할거에요 -> 독 속성이라면 + monster.ApplyStatusEffect(StatusEffectType.Poison, elementDamage, elementDuration); // 실행할거에요 -> 독 효과 적용을 + Debug.Log($"독 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을 break; - case ArrowElementType.Lightning: - monster.ApplyStatusEffect(StatusEffectType.Shock, elementDamage, elementDuration); - Debug.Log($"감전 효과! {elementDamage} 범위 데미지"); + case ArrowElementType.Lightning: // 조건이 맞으면 실행할거에요 -> 번개 속성이라면 + monster.ApplyStatusEffect(StatusEffectType.Shock, elementDamage, elementDuration); // 실행할거에요 -> 감전(스턴) 효과 적용을 + Debug.Log($"감전 효과! {elementDamage} 범위 데미지"); // 로그를 출력할거에요 -> 효과 설명을 break; } } @@ -247,58 +247,58 @@ public class PlayerArrow : MonoBehaviour /// /// 백업 충돌 감지 (OnTriggerEnter) — 기존 로직 유지 /// - private void OnTriggerEnter(Collider other) + private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거 충돌 시 OnTriggerEnter를 { - if (hasHit) return; + if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 맞았다면 - bool isEnemy = other.CompareTag("Enemy"); - int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); - if (!isEnemy && enemyHitBoxLayerIndex != -1) + bool isEnemy = other.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지 + int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 히트박스 레이어 인덱스를 + if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면 { - isEnemy = other.gameObject.layer == enemyHitBoxLayerIndex; + isEnemy = other.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 레이어 확인으로 } - if (isEnemy) + if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면 { - HandleHit(other, other.ClosestPoint(transform.position)); + HandleHit(other, other.ClosestPoint(transform.position)); // 실행할거에요 -> 충돌 처리 함수를 } - else if (other.CompareTag("Wall") || other.CompareTag("Ground")) + else if (other.CompareTag("Wall") || other.CompareTag("Ground")) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면 { - hasHit = true; - Destroy(gameObject, 0.05f); + hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로 + Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 } } /// /// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가 /// - private void OnDrawGizmos() + private void OnDrawGizmos() // 함수를 실행할거에요 -> 기즈모를 그리는 OnDrawGizmos를 { - if (!Application.isPlaying || !isFired || !showDebugRay) return; + if (!Application.isPlaying || !isFired || !showDebugRay) return; // 조건이 맞으면 중단할거에요 -> 실행 중이 아니거나 발사 전이거나 디버그가 꺼졌다면 - Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; + Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치로 Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f ? rb.velocity.normalized - : transform.forward; + : transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면으로 - Gizmos.color = hasHit ? Color.red : Color.green; - Gizmos.DrawWireSphere(tipPosition, raycastRadius); - Gizmos.DrawWireSphere(tipPosition + direction * raycastDistance, raycastRadius); + Gizmos.color = hasHit ? Color.red : Color.green; // 색상을 설정할거에요 -> 맞았으면 빨강, 아니면 초록으로 + Gizmos.DrawWireSphere(tipPosition, raycastRadius); // 그림을 그릴거에요 -> 팁 위치에 구체를 + Gizmos.DrawWireSphere(tipPosition + direction * raycastDistance, raycastRadius); // 그림을 그릴거에요 -> 예상 도달 위치에 구체를 - Gizmos.color = Color.yellow; - Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance); + Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로 + Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance); // 선을 그릴거에요 -> 궤적을 표시하는 // [NEW] 속성별 색상 표시 - switch (elementType) + switch (elementType) // 분기할거에요 -> 속성 타입에 따라 { - case ArrowElementType.Fire: Gizmos.color = Color.red; break; - case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; - case ArrowElementType.Poison: Gizmos.color = Color.green; break; - case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; + case ArrowElementType.Fire: Gizmos.color = Color.red; break; // 색상을 바꿀거에요 -> 빨강으로 + case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; // 색상을 바꿀거에요 -> 청록으로 + case ArrowElementType.Poison: Gizmos.color = Color.green; break; // 색상을 바꿀거에요 -> 초록으로 + case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; // 색상을 바꿀거에요 -> 노랑으로 } - if (elementType != ArrowElementType.None) + if (elementType != ArrowElementType.None) // 조건이 맞으면 실행할거에요 -> 속성이 있다면 { - Gizmos.DrawWireSphere(transform.position, 0.5f); + Gizmos.DrawWireSphere(transform.position, 0.5f); // 그림을 그릴거에요 -> 속성 표시용 구체를 } } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Combat/ArrowData.cs b/Assets/Scripts/Player/Combat/ArrowData.cs index bb28ceb6..bba9a055 100644 --- a/Assets/Scripts/Player/Combat/ArrowData.cs +++ b/Assets/Scripts/Player/Combat/ArrowData.cs @@ -1,46 +1,18 @@ -// ============================================ -// 📁 파일명: ArrowData.cs -// 📂 경로: Assets/Scripts/Data/ArrowData.cs -// ============================================ +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -using UnityEngine; - -/// -/// 화살 정보를 담는 데이터 구조체 -/// ArrowPickup.GetArrowData() → PlayerAttack.SetCurrentArrow() 로 전달됩니다. -/// -[System.Serializable] -public struct ArrowData +[CreateAssetMenu(fileName = "New Arrow Data", menuName = "Combat/Arrow Data")] // 메뉴를 만들거에요 -> 에셋 메뉴를 +public class ArrowData : ScriptableObject // 클래스를 선언할거에요 -> 데이터 컨테이너인 ArrowData를 { - [Header("--- 기본 정보 ---")] - public string arrowName; // 화살 이름 (UI 표시용) - public ArrowElementType elementType; // 속성 타입 + [Header("기본 정보")] // 인스펙터 창에 제목을 표시할거에요 -> 기본 정보 를 + public string arrowName; // 변수를 선언할거에요 -> 화살 이름을 + public GameObject projectilePrefab; // 변수를 선언할거에요 -> 투사체 프리팹을 + public Sprite icon; // 변수를 선언할거에요 -> 아이콘 이미지를 - [Header("--- 스탯 ---")] - public float baseDamage; // 기본 데미지 - public float elementDamage; // 속성 추가 데미지 - public float elementDuration; // 속성 지속시간 (초) + [Header("데미지 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 데미지 설정 을 + public float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 물리 데미지를 baseDamage에 (이게 빠져서 오류남) - [Header("--- 프리팹 ---")] - public GameObject projectilePrefab; // 발사할 파티클 프리팹 - - /// - /// 기본 화살 데이터 생성 - /// - public static ArrowData Default => new ArrowData - { - arrowName = "기본 화살", - elementType = ArrowElementType.None, - baseDamage = 10f, - elementDamage = 0f, - elementDuration = 0f, - projectilePrefab = null - }; - - public override string ToString() - { - return $"[{arrowName}] 속성={elementType}, " + - $"기본데미지={baseDamage}, 속성데미지={elementDamage}, " + - $"지속시간={elementDuration}s"; - } + [Header("속성 정보")] // 인스펙터 창에 제목을 표시할거에요 -> 속성 정보 를 + public ArrowElementType elementType; // 변수를 선언할거에요 -> 속성 타입을 + public float elementDamage; // 변수를 선언할거에요 -> 속성 추가 데미지를 + public float elementDuration; // 변수를 선언할거에요 -> 속성 지속 시간을 } \ No newline at end of file diff --git a/Assets/Scripts/Player/Combat/Attack.cs b/Assets/Scripts/Player/Combat/Attack.cs index 058b4ac3..e570e4ce 100644 --- a/Assets/Scripts/Player/Combat/Attack.cs +++ b/Assets/Scripts/Player/Combat/Attack.cs @@ -1,102 +1,102 @@ -using UnityEngine; -using System.Collections; -using System.Collections.Generic; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 -public class PlayerAttack : MonoBehaviour +public class PlayerAttack : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerAttack을 { - [Header("--- 활 설정 ---")] - [SerializeField] private Transform firePoint; - [SerializeField] private PlayerAnimator pAnim; + [Header("--- 활 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 활 설정 --- 을 + [SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 화살 발사 위치인 firePoint를 + [SerializeField] private PlayerAnimator pAnim; // 변수를 선언할거에요 -> 플레이어 애니메이터 스크립트인 pAnim을 - [Header("--- 스탯 참조 ---")] - [SerializeField] private Stats playerStats; + [Header("--- 스탯 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 스탯 참조 --- 를 + [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트인 playerStats를 - [Header("--- 일반 공격 (좌클릭) ---")] - [SerializeField] private float normalRange = 15f; - [SerializeField] private float normalSpeed = 20f; - [SerializeField] private float attackCooldown = 0.5f; + [Header("--- 일반 공격 (좌클릭) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 일반 공격 (좌클릭) --- 을 + [SerializeField] private float normalRange = 15f; // 변수를 선언할거에요 -> 일반 공격 사거리인 normalRange를 + [SerializeField] private float normalSpeed = 20f; // 변수를 선언할거에요 -> 일반 화살 속도인 normalSpeed를 + [SerializeField] private float attackCooldown = 0.5f; // 변수를 선언할거에요 -> 공격 쿨타임인 attackCooldown을 - [Header("--- 차징 공격 (우클릭) ---")] - [SerializeField] private float maxChargeTime = 2.0f; + [Header("--- 차징 공격 (우클릭) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 차징 공격 (우클릭) --- 을 + [SerializeField] private float maxChargeTime = 2.0f; // 변수를 선언할거에요 -> 최대 차징 시간인 maxChargeTime을 - [System.Serializable] - public struct ChargeStage + [System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정할 수 있게 구조체를 + public struct ChargeStage // 구조체를 정의할거에요 -> 차징 단계를 정의하는 ChargeStage를 { - public float chargeTime; - public float damageMult; - public float rangeMult; + public float chargeTime; // 변수를 선언할거에요 -> 도달 시간인 chargeTime을 + public float damageMult; // 변수를 선언할거에요 -> 데미지 배율인 damageMult를 + public float rangeMult; // 변수를 선언할거에요 -> 사거리/속도 배율인 rangeMult를 } - [SerializeField] private List chargeStages; + [SerializeField] private List chargeStages; // 리스트를 선언할거에요 -> 차징 단계 목록인 chargeStages를 - [Header("--- 에임 보정 설정 ---")] - [SerializeField] private bool enableAutoAim = true; - [SerializeField] private float autoAimRange = 15f; - [SerializeField] private float autoAimAngle = 45f; - [Range(0f, 1f)] - [SerializeField] private float aimAssistStrength = 0.4f; - [SerializeField] private LayerMask enemyLayer; - [SerializeField] private bool onlyMaxCharge = true; + [Header("--- 에임 보정 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 에임 보정 설정 --- 을 + [SerializeField] private bool enableAutoAim = true; // 변수를 선언할거에요 -> 자동 조준 사용 여부인 enableAutoAim을 + [SerializeField] private float autoAimRange = 15f; // 변수를 선언할거에요 -> 자동 조준 감지 거리인 autoAimRange를 + [SerializeField] private float autoAimAngle = 45f; // 변수를 선언할거에요 -> 자동 조준 감지 각도인 autoAimAngle을 + [Range(0f, 1f)] // 슬라이더를 만들거에요 -> 0부터 1 사이의 값으로 + [SerializeField] private float aimAssistStrength = 0.4f; // 변수를 선언할거에요 -> 보정 강도인 aimAssistStrength를 + [SerializeField] private LayerMask enemyLayer; // 변수를 선언할거에요 -> 적 레이어인 enemyLayer를 + [SerializeField] private bool onlyMaxCharge = true; // 변수를 선언할거에요 -> 풀차징 때만 유도할지 여부인 onlyMaxCharge를 // ============================================ // [NEW] 파티클 기반 화살 시스템 // ============================================ - [Header("--- 파티클 화살 시스템 ---")] - [SerializeField] private GameObject defaultProjectilePrefab; // 기본 발사체 프리팹 + [Header("--- 파티클 화살 시스템 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 파티클 화살 시스템 --- 을 + [SerializeField] private GameObject defaultProjectilePrefab; // 변수를 선언할거에요 -> 기본 발사체 프리팹인 defaultProjectilePrefab을 - private GameObject _currentProjectilePrefab; - private ArrowElementType _currentElementType = ArrowElementType.None; - private float _currentElementDamage = 0f; - private float _currentElementDuration = 0f; + private GameObject _currentProjectilePrefab; // 변수를 선언할거에요 -> 현재 사용할 발사체 프리팹인 _currentProjectilePrefab을 + private ArrowElementType _currentElementType = ArrowElementType.None; // 변수를 초기화할거에요 -> 현재 화살 속성인 _currentElementType을 없음으로 + private float _currentElementDamage = 0f; // 변수를 초기화할거에요 -> 현재 속성 데미지인 _currentElementDamage를 0으로 + private float _currentElementDuration = 0f; // 변수를 초기화할거에요 -> 현재 속성 지속 시간인 _currentElementDuration을 0으로 - private float _lastAttackTime; - private float _chargeTimer; - private bool _isCharging = false; - private bool _isAttacking = false; - private bool _waitForRelease = false; + private float _lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간인 _lastAttackTime을 + private float _chargeTimer; // 변수를 선언할거에요 -> 차징 진행 시간인 _chargeTimer를 + private bool _isCharging = false; // 변수를 초기화할거에요 -> 차징 중 여부인 _isCharging을 거짓으로 + private bool _isAttacking = false; // 변수를 초기화할거에요 -> 공격 동작 중 여부인 _isAttacking을 거짓으로 + private bool _waitForRelease = false; // 변수를 초기화할거에요 -> 발사 대기 상태 여부인 _waitForRelease를 거짓으로 - private float _pendingDamage; - private float _pendingSpeed; - private float _pendingRange; - private Vector3 _pendingShootDirection; + private float _pendingDamage; // 변수를 선언할거에요 -> 발사될 화살의 데미지인 _pendingDamage를 + private float _pendingSpeed; // 변수를 선언할거에요 -> 발사될 화살의 속도인 _pendingSpeed를 + private float _pendingRange; // 변수를 선언할거에요 -> 발사될 화살의 사거리인 _pendingRange를 + private Vector3 _pendingShootDirection; // 변수를 선언할거에요 -> 발사될 방향인 _pendingShootDirection을 - public bool IsCharging => _isCharging; - public bool IsAttacking { get => _isAttacking; set => _isAttacking = value; } - public float ChargeProgress => Mathf.Clamp01(_chargeTimer / maxChargeTime); + public bool IsCharging => _isCharging; // 프로퍼티를 선언할거에요 -> 차징 중인지 여부를 반환하는 IsCharging을 + public bool IsAttacking { get => _isAttacking; set => _isAttacking = value; } // 프로퍼티를 선언할거에요 -> 공격 중인지 여부를 읽고 쓰는 IsAttacking을 + public float ChargeProgress => Mathf.Clamp01(_chargeTimer / maxChargeTime); // 프로퍼티를 선언할거에요 -> 차징 진행률(0~1)을 반환하는 ChargeProgress를 // [NEW] 외부에서 현재 속성 확인용 - public ArrowElementType CurrentElement => _currentElementType; - public GameObject CurrentProjectilePrefab => _currentProjectilePrefab; + public ArrowElementType CurrentElement => _currentElementType; // 프로퍼티를 선언할거에요 -> 현재 화살 속성을 반환하는 CurrentElement를 + public GameObject CurrentProjectilePrefab => _currentProjectilePrefab; // 프로퍼티를 선언할거에요 -> 현재 발사체 프리팹을 반환하는 CurrentProjectilePrefab을 - private void Start() + private void Start() // 함수를 실행할거에요 -> 스크립트 시작 시 Start를 { - if (playerStats == null) playerStats = GetComponentInParent(); + if (playerStats == null) playerStats = GetComponentInParent(); // 조건이 맞으면 가져올거에요 -> 부모의 Stats 컴포넌트를 playerStats에 // 기본 발사체 프리팹 설정 - if (_currentProjectilePrefab == null) - _currentProjectilePrefab = defaultProjectilePrefab; + if (_currentProjectilePrefab == null) // 조건이 맞으면 실행할거에요 -> 현재 설정된 프리팹이 없다면 + _currentProjectilePrefab = defaultProjectilePrefab; // 값을 넣을거에요 -> 기본 프리팹을 - if (chargeStages == null || chargeStages.Count == 0) + if (chargeStages == null || chargeStages.Count == 0) // 조건이 맞으면 실행할거에요 -> 차징 단계 리스트가 비어있다면 { - chargeStages = new List + chargeStages = new List // 리스트를 생성할거에요 -> 기본 3단계 차징 설정을 담은 리스트를 { - new ChargeStage { chargeTime = 0f, damageMult = 1f, rangeMult = 1f }, - new ChargeStage { chargeTime = 1f, damageMult = 1.5f, rangeMult = 1.2f }, - new ChargeStage { chargeTime = 2f, damageMult = 2.5f, rangeMult = 1.5f } + new ChargeStage { chargeTime = 0f, damageMult = 1f, rangeMult = 1f }, // 값을 넣을거에요 -> 0단계 설정을 + new ChargeStage { chargeTime = 1f, damageMult = 1.5f, rangeMult = 1.2f }, // 값을 넣을거에요 -> 1단계 설정을 + new ChargeStage { chargeTime = 2f, damageMult = 2.5f, rangeMult = 1.5f } // 값을 넣을거에요 -> 2단계 설정을 }; } - float calculatedMaxTime = 0f; - foreach (var stage in chargeStages) + float calculatedMaxTime = 0f; // 변수를 초기화할거에요 -> 계산된 최대 시간을 0으로 + foreach (var stage in chargeStages) // 반복할거에요 -> 모든 차징 단계에 대해 { - if (stage.chargeTime > calculatedMaxTime) calculatedMaxTime = stage.chargeTime; + if (stage.chargeTime > calculatedMaxTime) calculatedMaxTime = stage.chargeTime; // 조건이 맞으면 갱신할거에요 -> 더 긴 시간이 있다면 그 값으로 } - maxChargeTime = Mathf.Max(calculatedMaxTime, 0.1f); + maxChargeTime = Mathf.Max(calculatedMaxTime, 0.1f); // 값을 설정할거에요 -> 계산된 최대 시간으로 (최소 0.1 보장) } - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를 { - if (_isCharging) _chargeTimer += Time.deltaTime; + if (_isCharging) _chargeTimer += Time.deltaTime; // 조건이 맞으면 더할거에요 -> 차징 중이라면 타이머에 시간을 } // ============================================ @@ -107,165 +107,165 @@ public class PlayerAttack : MonoBehaviour /// ArrowPickup에서 습득 시 호출합니다. /// 파티클 프리팹 + 속성 정보를 저장합니다. /// - public void SetCurrentArrow(ArrowData data) + public void SetCurrentArrow(ArrowData data) // 함수를 선언할거에요 -> 새로운 화살 데이터를 적용하는 SetCurrentArrow를 { - if (data.projectilePrefab != null) - _currentProjectilePrefab = data.projectilePrefab; - else - _currentProjectilePrefab = defaultProjectilePrefab; + if (data.projectilePrefab != null) // 조건이 맞으면 실행할거에요 -> 데이터에 프리팹이 있다면 + _currentProjectilePrefab = data.projectilePrefab; // 값을 바꿀거에요 -> 현재 발사체 프리팹을 새 것으로 + else // 조건이 틀리면 실행할거에요 -> 데이터에 프리팹이 없다면 + _currentProjectilePrefab = defaultProjectilePrefab; // 값을 바꿀거에요 -> 기본 프리팹으로 - _currentElementType = data.elementType; - _currentElementDamage = data.elementDamage; - _currentElementDuration = data.elementDuration; + _currentElementType = data.elementType; // 값을 저장할거에요 -> 화살 속성 타입을 + _currentElementDamage = data.elementDamage; // 값을 저장할거에요 -> 속성 데미지를 + _currentElementDuration = data.elementDuration; // 값을 저장할거에요 -> 속성 지속 시간을 Debug.Log($"화살 장착: [{data.arrowName}] 속성={data.elementType}, " + - $"속성데미지={data.elementDamage}, 지속시간={data.elementDuration}s"); + $"속성데미지={data.elementDamage}, 지속시간={data.elementDuration}s"); // 로그를 출력할거에요 -> 장착된 화살 정보를 } /// /// 기본 화살로 초기화 /// - public void ResetArrow() + public void ResetArrow() // 함수를 선언할거에요 -> 화살을 기본 상태로 되돌리는 ResetArrow를 { - _currentProjectilePrefab = defaultProjectilePrefab; - _currentElementType = ArrowElementType.None; - _currentElementDamage = 0f; - _currentElementDuration = 0f; - Debug.Log("화살 초기화: 기본 화살"); + _currentProjectilePrefab = defaultProjectilePrefab; // 값을 바꿀거에요 -> 발사체를 기본 프리팹으로 + _currentElementType = ArrowElementType.None; // 값을 바꿀거에요 -> 속성을 없음(None)으로 + _currentElementDamage = 0f; // 값을 초기화할거에요 -> 속성 데미지를 0으로 + _currentElementDuration = 0f; // 값을 초기화할거에요 -> 속성 시간을 0으로 + Debug.Log("화살 초기화: 기본 화살"); // 로그를 출력할거에요 -> 초기화 완료 메시지를 } // ============================================ // 일반 공격 (좌클릭) — 기존 로직 100% 유지 // ============================================ - public void PerformNormalAttack() + public void PerformNormalAttack() // 함수를 선언할거에요 -> 일반 공격을 수행하는 PerformNormalAttack을 { - if (Time.time < _lastAttackTime + attackCooldown || _isAttacking) return; + if (Time.time < _lastAttackTime + attackCooldown || _isAttacking) return; // 조건이 맞으면 중단할거에요 -> 쿨타임 중이거나 이미 공격 중이라면 - _pendingDamage = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; - _pendingSpeed = normalSpeed; - _pendingRange = normalRange; + _pendingDamage = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; // 값을 가져올거에요 -> 스탯이 있으면 총 공격력을, 없으면 10을 + _pendingSpeed = normalSpeed; // 값을 저장할거에요 -> 기본 속도를 + _pendingRange = normalRange; // 값을 저장할거에요 -> 기본 사거리를 - _pendingShootDirection = GetMouseDirection(); - _lastAttackTime = Time.time; + _pendingShootDirection = GetMouseDirection(); // 값을 계산할거에요 -> 마우스 방향을 구해서 발사 방향으로 + _lastAttackTime = Time.time; // 값을 갱신할거에요 -> 마지막 공격 시간을 현재로 - if (pAnim != null) pAnim.TriggerThrow(); - StartCoroutine(AttackRoutine()); + if (pAnim != null) pAnim.TriggerThrow(); // 실행할거에요 -> 던지는 애니메이션 트리거를 + StartCoroutine(AttackRoutine()); // 코루틴을 시작할거에요 -> 공격 상태 관리를 위한 AttackRoutine을 } // ============================================ // 차징 공격 (우클릭) — 기존 로직 100% 유지 // ============================================ - public void StartCharging() + public void StartCharging() // 함수를 선언할거에요 -> 차징을 시작하는 StartCharging을 { - if (_waitForRelease) return; - _isCharging = true; - _chargeTimer = 0f; - if (pAnim != null) pAnim.SetCharging(true); - if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true); + if (_waitForRelease) return; // 조건이 맞으면 중단할거에요 -> 이미 발사 대기 중이라면 + _isCharging = true; // 상태를 바꿀거에요 -> 차징 중 상태를 참(true)으로 + _chargeTimer = 0f; // 값을 초기화할거에요 -> 차징 타이머를 0으로 + if (pAnim != null) pAnim.SetCharging(true); // 실행할거에요 -> 애니메이터에 차징 시작을 알리기를 + if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true); // 조건이 맞으면 실행할거에요 -> 카메라 줌 효과를 켜기를 } - private void ResetChargingEffects() + private void ResetChargingEffects() // 함수를 선언할거에요 -> 차징 효과를 초기화하는 ResetChargingEffects를 { - _isCharging = false; - _chargeTimer = 0f; - if (pAnim != null) pAnim.SetCharging(false); - if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false); + _isCharging = false; // 상태를 바꿀거에요 -> 차징 상태를 거짓(false)으로 + _chargeTimer = 0f; // 값을 초기화할거에요 -> 차징 타이머를 + if (pAnim != null) pAnim.SetCharging(false); // 실행할거에요 -> 애니메이터 차징 상태 끄기를 + if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false); // 조건이 맞으면 실행할거에요 -> 카메라 줌 효과를 끄기를 } - public void CancelCharging() + public void CancelCharging() // 함수를 선언할거에요 -> 차징을 취소하는 CancelCharging을 { - ResetChargingEffects(); - _waitForRelease = false; + ResetChargingEffects(); // 함수를 실행할거에요 -> 차징 효과 초기화를 + _waitForRelease = false; // 상태를 바꿀거에요 -> 발사 대기 상태를 거짓으로 } - public void ReleaseAttack() + public void ReleaseAttack() // 함수를 선언할거에요 -> 차징된 공격을 발사하는 ReleaseAttack을 { - if (!_isCharging) return; + if (!_isCharging) return; // 조건이 맞으면 중단할거에요 -> 차징 중이 아니라면 - ChargeStage currentStage = chargeStages[0]; - foreach (var stage in chargeStages) + ChargeStage currentStage = chargeStages[0]; // 변수를 초기화할거에요 -> 기본 0단계를 시작값으로 + foreach (var stage in chargeStages) // 반복할거에요 -> 모든 단계를 돌면서 { - if (_chargeTimer >= stage.chargeTime) currentStage = stage; + if (_chargeTimer >= stage.chargeTime) currentStage = stage; // 조건이 맞으면 갱신할거에요 -> 타이머가 단계 시간보다 길다면 해당 단계로 } - float baseDmg = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; - _pendingDamage = baseDmg * currentStage.damageMult; - _pendingSpeed = normalSpeed * currentStage.rangeMult; - _pendingRange = normalRange * currentStage.rangeMult; + float baseDmg = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; // 값을 가져올거에요 -> 기본 공격력을 + _pendingDamage = baseDmg * currentStage.damageMult; // 값을 계산할거에요 -> 배율을 적용한 최종 데미지를 + _pendingSpeed = normalSpeed * currentStage.rangeMult; // 값을 계산할거에요 -> 배율을 적용한 속도를 + _pendingRange = normalRange * currentStage.rangeMult; // 값을 계산할거에요 -> 배율을 적용한 사거리를 - bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f; - _pendingShootDirection = GetShootDirection(isMaxCharge); + bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f; // 조건을 확인할거에요 -> 풀차징(95% 이상)인지 여부를 + _pendingShootDirection = GetShootDirection(isMaxCharge); // 값을 계산할거에요 -> 보정이 적용된 발사 방향을 - if (pAnim != null) pAnim.TriggerThrow(); - _waitForRelease = true; - _lastAttackTime = Time.time; - StartCoroutine(AttackRoutine()); + if (pAnim != null) pAnim.TriggerThrow(); // 실행할거에요 -> 던지는 애니메이션을 + _waitForRelease = true; // 상태를 바꿀거에요 -> 발사 대기 상태를 참으로 + _lastAttackTime = Time.time; // 값을 갱신할거에요 -> 마지막 공격 시간을 + StartCoroutine(AttackRoutine()); // 코루틴을 시작할거에요 -> 공격 루틴을 } // ============================================ // 에임 보정 시스템 — 기존 로직 100% 유지 // ============================================ - private Vector3 GetMouseDirection() + private Vector3 GetMouseDirection() // 함수를 선언할거에요 -> 마우스가 가리키는 방향을 구하는 GetMouseDirection을 { - Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); - Plane firePlane = new Plane(Vector3.up, firePoint.position); + Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 레이를 쏠거에요 -> 카메라에서 마우스 위치로 + Plane firePlane = new Plane(Vector3.up, firePoint.position); // 평면을 만들거에요 -> 발사 위치 높이의 수평면을 - if (firePlane.Raycast(ray, out float distance)) + if (firePlane.Raycast(ray, out float distance)) // 조건이 맞으면 실행할거에요 -> 레이가 평면에 닿았다면 { - Vector3 worldMousePos = ray.GetPoint(distance); - Vector3 direction = (worldMousePos - firePoint.position).normalized; - direction.y = 0; - return direction; + Vector3 worldMousePos = ray.GetPoint(distance); // 위치를 구할거에요 -> 평면상 마우스 위치를 + Vector3 direction = (worldMousePos - firePoint.position).normalized; // 벡터를 구할거에요 -> 발사 위치에서 마우스까지의 방향을 + direction.y = 0; // 값을 바꿀거에요 -> 높이 차이는 무시하게 y를 0으로 + return direction; // 반환할거에요 -> 계산된 방향을 } - return transform.forward; + return transform.forward; // 반환할거에요 -> 실패 시 정면 방향을 } - private Vector3 GetShootDirection(bool isMaxCharge) + private Vector3 GetShootDirection(bool isMaxCharge) // 함수를 선언할거에요 -> 최종 발사 방향(보정 포함)을 구하는 GetShootDirection을 { - Vector3 mouseDir = GetMouseDirection(); - if (!enableAutoAim || (onlyMaxCharge && !isMaxCharge)) return mouseDir; + Vector3 mouseDir = GetMouseDirection(); // 값을 가져올거에요 -> 마우스 방향을 + if (!enableAutoAim || (onlyMaxCharge && !isMaxCharge)) return mouseDir; // 조건이 맞으면 반환할거에요 -> 보정 안 씀 설정이거나 풀차징이 아니면 마우스 방향 그대로 - Transform bestTarget = FindBestTarget(mouseDir); - if (bestTarget != null) + Transform bestTarget = FindBestTarget(mouseDir); // 함수를 실행할거에요 -> 가장 적합한 타겟을 찾는 FindBestTarget을 + if (bestTarget != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 { - Vector3 targetPos = bestTarget.position + Vector3.up * 1.2f; - Vector3 targetDir = (targetPos - firePoint.position).normalized; - targetDir.y = 0; + Vector3 targetPos = bestTarget.position + Vector3.up * 1.2f; // 위치를 계산할거에요 -> 타겟의 중심부(높이 보정)를 + Vector3 targetDir = (targetPos - firePoint.position).normalized; // 방향을 계산할거에요 -> 발사 위치에서 타겟까지 + targetDir.y = 0; // 값을 바꿀거에요 -> 수평 방향만 고려하게 - Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); - assistedDir.Normalize(); - return assistedDir; + Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); // 보간할거에요 -> 마우스 방향과 타겟 방향 사이를 강도만큼 + assistedDir.Normalize(); // 정규화할거에요 -> 벡터 길이를 1로 + return assistedDir; // 반환할거에요 -> 보정된 방향을 } - return mouseDir; + return mouseDir; // 반환할거에요 -> 타겟 없으면 마우스 방향을 } - private Transform FindBestTarget(Vector3 mouseDirection) + private Transform FindBestTarget(Vector3 mouseDirection) // 함수를 선언할거에요 -> 조준 보정 대상을 찾는 FindBestTarget을 { - Collider[] enemies = Physics.OverlapSphere(transform.position, autoAimRange, enemyLayer); - if (enemies.Length == 0) return null; + Collider[] enemies = Physics.OverlapSphere(transform.position, autoAimRange, enemyLayer); // 배열에 담을거에요 -> 사거리 내의 모든 적을 + if (enemies.Length == 0) return null; // 조건이 맞으면 반환할거에요 -> 적이 없으면 null을 - Transform bestTarget = null; - float bestScore = float.MaxValue; + Transform bestTarget = null; // 변수를 초기화할거에요 -> 최고 타겟을 비워두고 + float bestScore = float.MaxValue; // 변수를 초기화할거에요 -> 점수를 최대값으로 - foreach (var enemy in enemies) + foreach (var enemy in enemies) // 반복할거에요 -> 모든 적에 대해 { - Vector3 dirToEnemy = (enemy.transform.position - transform.position).normalized; - float angle = Vector3.Angle(mouseDirection, dirToEnemy); - if (angle > autoAimAngle) continue; + Vector3 dirToEnemy = (enemy.transform.position - transform.position).normalized; // 방향을 구할거에요 -> 적을 향하는 방향을 + float angle = Vector3.Angle(mouseDirection, dirToEnemy); // 각도를 잴거에요 -> 마우스 방향과 적 방향 사이를 + if (angle > autoAimAngle) continue; // 조건이 맞으면 건너뛸거에요 -> 시야각 밖이라면 - float distance = Vector3.Distance(transform.position, enemy.transform.position); - float score = angle * 0.5f + distance * 0.5f; + float distance = Vector3.Distance(transform.position, enemy.transform.position); // 거리를 잴거에요 -> 적까지의 거리를 + float score = angle * 0.5f + distance * 0.5f; // 점수를 매길거에요 -> 각도와 거리를 합산해서 (낮을수록 좋음) - if (score < bestScore) + if (score < bestScore) // 조건이 맞으면 갱신할거에요 -> 현재 점수가 최고 점수보다 낮다면 { - bestScore = score; - bestTarget = enemy.transform; + bestScore = score; // 값을 저장할거에요 -> 새 점수를 + bestTarget = enemy.transform; // 값을 저장할거에요 -> 새 타겟을 } } - return bestTarget; + return bestTarget; // 반환할거에요 -> 가장 점수가 좋은 타겟을 } // ============================================ @@ -276,27 +276,27 @@ public class PlayerAttack : MonoBehaviour /// 애니메이션 이벤트에서 호출됩니다. /// 파티클 프리팹을 Instantiate하고 PlayerArrow 컴포넌트로 이동/충돌을 처리합니다. /// - public void OnShootArrow() + public void OnShootArrow() // 함수를 선언할거에요 -> 애니메이션 이벤트로 호출될 발사 함수 OnShootArrow를 { - if (_currentProjectilePrefab == null || firePoint == null) + if (_currentProjectilePrefab == null || firePoint == null) // 조건이 맞으면 중단할거에요 -> 프리팹이나 발사 위치가 없다면 { - Debug.LogWarning("발사체 프리팹 또는 firePoint가 없습니다!"); + Debug.LogWarning("발사체 프리팹 또는 firePoint가 없습니다!"); // 경고 로그를 띄울거에요 -> 설정 확인 필요 메시지를 return; } // 파티클 프리팹을 발사 위치에 생성 - Quaternion rotation = Quaternion.LookRotation(_pendingShootDirection); - GameObject projectile = Instantiate(_currentProjectilePrefab, firePoint.position, rotation); + Quaternion rotation = Quaternion.LookRotation(_pendingShootDirection); // 회전을 계산할거에요 -> 발사 방향을 바라보게 + GameObject projectile = Instantiate(_currentProjectilePrefab, firePoint.position, rotation); // 생성할거에요 -> 화살 오브젝트를 // PlayerArrow 컴포넌트 확인/추가 후 초기화 - PlayerArrow arrowScript = projectile.GetComponent(); - if (arrowScript == null) + PlayerArrow arrowScript = projectile.GetComponent(); // 컴포넌트를 가져올거에요 -> PlayerArrow 스크립트를 + if (arrowScript == null) // 조건이 맞으면 실행할거에요 -> 스크립트가 없다면 { - arrowScript = projectile.AddComponent(); + arrowScript = projectile.AddComponent(); // 추가할거에요 -> PlayerArrow 컴포넌트를 즉석에서 } // 발사 정보 + 속성 정보 전달 - arrowScript.Initialize( + arrowScript.Initialize( // 초기화할거에요 -> 화살 스크립트에 모든 정보를 전달해서 _pendingDamage, _pendingSpeed, _pendingRange, @@ -311,63 +311,63 @@ public class PlayerAttack : MonoBehaviour // 유틸리티 — 기존 로직 100% 유지 // ============================================ - public void OnAttackEnd() + public void OnAttackEnd() // 함수를 선언할거에요 -> 공격 애니메이션 종료 시 호출될 OnAttackEnd를 { - _isAttacking = false; - ResetChargingEffects(); + _isAttacking = false; // 상태를 바꿀거에요 -> 공격 상태를 거짓으로 + ResetChargingEffects(); // 함수를 실행할거에요 -> 차징 효과 초기화를 } - private IEnumerator AttackRoutine() + private IEnumerator AttackRoutine() // 코루틴 함수를 선언할거에요 -> 공격 상태 안전장치인 AttackRoutine을 { - _isAttacking = true; - yield return new WaitForSeconds(0.6f); - if (_isAttacking) + _isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참으로 + yield return new WaitForSeconds(0.6f); // 기다릴거에요 -> 0.6초(모션 시간)만큼 + if (_isAttacking) // 조건이 맞으면 실행할거에요 -> 아직도 공격 중 상태라면 (강제 종료) { - _isAttacking = false; - ResetChargingEffects(); + _isAttacking = false; // 상태를 바꿀거에요 -> 공격 상태를 거짓으로 + ResetChargingEffects(); // 함수를 실행할거에요 -> 효과 초기화를 } } // [DEPRECATED] 하위 호환성을 위해 남겨둠 - [System.Obsolete("SetCurrentArrow(ArrowData)를 사용하세요.")] - public void SwapArrow(GameObject newArrow) + [System.Obsolete("SetCurrentArrow(ArrowData)를 사용하세요.")] // 경고를 띄울거에요 -> 이 함수는 더 이상 쓰지 말라고 + public void SwapArrow(GameObject newArrow) // 함수를 선언할거에요 -> 구버전 화살 교체 함수 SwapArrow를 { - if (newArrow == null) return; - Debug.LogWarning("SwapArrow()는 더 이상 사용되지 않습니다. SetCurrentArrow()를 사용하세요."); + if (newArrow == null) return; // 조건이 맞으면 중단할거에요 -> 비어있다면 + Debug.LogWarning("SwapArrow()는 더 이상 사용되지 않습니다. SetCurrentArrow()를 사용하세요."); // 경고 로그를 출력할거에요 -> 새 함수 사용 권장을 } - public void StartWeaponCollision() { } - public void StopWeaponCollision() { } + public void StartWeaponCollision() { } // 함수를 비워둘거에요 -> 근접 무기 충돌 시작(활에는 필요 없음) + public void StopWeaponCollision() { } // 함수를 비워둘거에요 -> 근접 무기 충돌 종료(활에는 필요 없음) // ============================================ // Gizmo 디버그 — 기존 로직 100% 유지 // ============================================ - private void OnDrawGizmosSelected() + private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터 선택 시 디버그 그림을 그리는 OnDrawGizmosSelected를 { - if (!enableAutoAim) return; - Gizmos.color = Color.yellow; - Gizmos.DrawWireSphere(transform.position, autoAimRange); + if (!enableAutoAim) return; // 조건이 맞으면 중단할거에요 -> 자동 조준이 꺼져있다면 + Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로 + Gizmos.DrawWireSphere(transform.position, autoAimRange); // 그림을 그릴거에요 -> 자동 조준 범위를 - if (Application.isPlaying && firePoint != null) + if (Application.isPlaying && firePoint != null) // 조건이 맞으면 실행할거에요 -> 게임 실행 중이고 발사 위치가 있다면 { - Vector3 mouseDir = GetMouseDirection(); - Gizmos.color = Color.blue; - Gizmos.DrawRay(firePoint.position, mouseDir * 5f); + Vector3 mouseDir = GetMouseDirection(); // 값을 가져올거에요 -> 마우스 방향을 + Gizmos.color = Color.blue; // 색상을 설정할거에요 -> 파란색으로 + Gizmos.DrawRay(firePoint.position, mouseDir * 5f); // 선을 그릴거에요 -> 마우스 방향 레이를 - Transform target = FindBestTarget(mouseDir); - if (target != null) + Transform target = FindBestTarget(mouseDir); // 값을 가져올거에요 -> 타겟을 + if (target != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 { - Vector3 targetPos = target.position + Vector3.up * 1.2f; - Vector3 targetDir = (targetPos - firePoint.position).normalized; - targetDir.y = 0; - Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); - assistedDir.Normalize(); + Vector3 targetPos = target.position + Vector3.up * 1.2f; // 위치를 계산할거에요 -> 타겟 중심점을 + Vector3 targetDir = (targetPos - firePoint.position).normalized; // 방향을 계산할거에요 -> 타겟 방향을 + targetDir.y = 0; // 값을 바꿀거에요 -> 수평으로 + Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); // 보간할거에요 -> 보정된 방향을 + assistedDir.Normalize(); // 정규화할거에요 -> 벡터를 - Gizmos.color = Color.red; - Gizmos.DrawRay(firePoint.position, assistedDir * 7f); - Gizmos.color = Color.green; - Gizmos.DrawWireSphere(targetPos, 0.3f); + Gizmos.color = Color.red; // 색상을 설정할거에요 -> 빨간색으로 + Gizmos.DrawRay(firePoint.position, assistedDir * 7f); // 선을 그릴거에요 -> 최종 발사 방향을 + Gizmos.color = Color.green; // 색상을 설정할거에요 -> 초록색으로 + Gizmos.DrawWireSphere(targetPos, 0.3f); // 그림을 그릴거에요 -> 타겟 위치 표시를 } } } diff --git a/Assets/Scripts/Player/Combat/StatusEffectType.cs b/Assets/Scripts/Player/Combat/StatusEffectType.cs index 76370753..693432e0 100644 --- a/Assets/Scripts/Player/Combat/StatusEffectType.cs +++ b/Assets/Scripts/Player/Combat/StatusEffectType.cs @@ -9,6 +9,7 @@ /// public enum StatusEffectType { + None, // ⭐ [추가됨] 아무 상태이상 없음 (기본값) Burn, // 화상 - 일정 시간 동안 틱 데미지 Slow, // 슬로우 - 이동속도 감소 Poison, // 중독 - 틱 데미지 + 방어력 감소 diff --git a/Assets/Scripts/Player/Controller/ArrowPickup.cs b/Assets/Scripts/Player/Controller/ArrowPickup.cs index 46d5c1de..d1e091ee 100644 --- a/Assets/Scripts/Player/Controller/ArrowPickup.cs +++ b/Assets/Scripts/Player/Controller/ArrowPickup.cs @@ -1,228 +1,229 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -/// -/// 바닥에 떨어진 화살 아이템 (아이템 전용 — 발사체 기능 제거됨) -/// 습득 시 PlayerAttack에 파티클 프리팹 + 속성 정보를 전달합니다. -/// -public class ArrowPickup : MonoBehaviour -{ - [Header("--- 화살 아이템 정보 ---")] - [SerializeField] private string arrowName = "기본 화살"; - [SerializeField] private ArrowElementType elementType = ArrowElementType.None; +/// // 요약 주석을 시작할거에요 -> 클래스 설명을 +/// 바닥에 떨어진 화살 아이템 (아이템 전용 — 발사체 기능 제거됨) // 설명할거에요 -> 이 스크립트 역할을 +/// 습득 시 PlayerAttack에 파티클 프리팹 + 속성 정보를 전달합니다. // 설명할거에요 -> 습득 시 전달 내용 +/// // 요약 주석을 끝낼거에요 -> 클래스 설명을 +public class ArrowPickup : MonoBehaviour // 클래스를 선언할거에요 -> 화살 아이템 습득/표시를 담당하는 ArrowPickup을 +{ // 코드 블록을 시작할거에요 -> ArrowPickup 범위를 - [Header("--- 스탯 ---")] - [SerializeField] private float baseDamage = 15f; - [SerializeField] private float elementDamage = 5f; - [SerializeField] private float elementDuration = 3f; + [Header("--- 화살 아이템 정보 ---")] // 인스펙터에 제목을 표시할거에요 -> 화살 아이템 정보 섹션을 + [SerializeField] private string arrowName = "기본 화살"; // 변수를 선언할거에요 -> 화살 이름을 arrowName에 + [SerializeField] private ArrowElementType elementType = ArrowElementType.None; // 변수를 선언할거에요 -> 화살 속성 타입을 elementType에 - [Header("--- 발사체 파티클 ---")] - [Tooltip("이 화살이 장착될 때 발사할 파티클 프리팹")] - [SerializeField] private GameObject projectilePrefab; + [Header("--- 스탯 ---")] // 인스펙터에 제목을 표시할거에요 -> 스탯 섹션을 + [SerializeField] private float baseDamage = 15f; // 변수를 선언할거에요 -> 기본 데미지를 baseDamage에 + [SerializeField] private float elementDamage = 5f; // 변수를 선언할거에요 -> 속성 데미지를 elementDamage에 + [SerializeField] private float elementDuration = 3f; // 변수를 선언할거에요 -> 속성 지속시간을 elementDuration에 - [Header("--- 비주얼 (필드 아이템) ---")] - [SerializeField] private GameObject visualModel; + [Header("--- 발사체 파티클 ---")] // 인스펙터에 제목을 표시할거에요 -> 파티클 섹션을 + [Tooltip("이 화살이 장착될 때 발사할 파티클 프리팹")] // 인스펙터에 툴팁을 달거에요 -> projectilePrefab 설명을 + [SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 장착 시 쓸 파티클 프리팹을 projectilePrefab에 - [Header("--- UI 표시 ---")] - [SerializeField] private GameObject pickupUI; - [SerializeField] private float uiDisplayRange = 3f; + [Header("--- 비주얼 (필드 아이템) ---")] // 인스펙터에 제목을 표시할거에요 -> 비주얼 섹션을 + [SerializeField] private GameObject visualModel; // 변수를 선언할거에요 -> 필드에서 보일 모델을 visualModel에 - [Header("--- Collider 설정 (자동 탐지) ---")] - [Tooltip("아이템 습득용 Sphere Collider (Trigger)")] - [SerializeField] private Collider pickupCollider; - [Tooltip("바닥 충돌용 Box Collider (물리)")] - [SerializeField] private Collider physicsCollider; + [Header("--- UI 표시 ---")] // 인스펙터에 제목을 표시할거에요 -> UI 표시 섹션을 + [SerializeField] private GameObject pickupUI; // 변수를 선언할거에요 -> 습득 UI 오브젝트를 pickupUI에 + [SerializeField] private float uiDisplayRange = 3f; // 변수를 선언할거에요 -> UI 표시 거리를 uiDisplayRange에 - private Transform playerTransform; - private Rigidbody rb; + [Header("--- Collider 설정 (자동 탐지) ---")] // 인스펙터에 제목을 표시할거에요 -> 콜라이더 설정 섹션을 + [Tooltip("아이템 습득용 Sphere Collider (Trigger)")] // 인스펙터에 툴팁을 달거에요 -> pickupCollider 설명을 + [SerializeField] private Collider pickupCollider; // 변수를 선언할거에요 -> 습득용 트리거 콜라이더를 pickupCollider에 + [Tooltip("바닥 충돌용 Box Collider (물리)")] // 인스펙터에 툴팁을 달거에요 -> physicsCollider 설명을 + [SerializeField] private Collider physicsCollider; // 변수를 선언할거에요 -> 물리 충돌 콜라이더를 physicsCollider에 - /// - /// [NEW] ArrowData 구조체로 정보 패키징 - /// - public ArrowData GetArrowData() - { - return new ArrowData - { - arrowName = this.arrowName, - elementType = this.elementType, - baseDamage = this.baseDamage, - elementDamage = this.elementDamage, - elementDuration = this.elementDuration, - projectilePrefab = this.projectilePrefab - }; - } + private Transform playerTransform; // 변수를 선언할거에요 -> 플레이어 트랜스폼을 저장할 playerTransform을 + private Rigidbody rb; // 변수를 선언할거에요 -> 리지드바디를 저장할 rb를 - private void Awake() - { - rb = GetComponent(); + /// // 요약 주석을 시작할거에요 -> ArrowData 패키징 설명을 + /// [NEW] ArrowData 구조체로 정보 패키징 // 설명할거에요 -> PlayerAttack에 넘기기 쉽게 묶는 함수 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + public ArrowData GetArrowData() // 함수를 선언할거에요 -> 현재 화살 정보를 ArrowData로 반환하는 GetArrowData를 + { // 코드 블록을 시작할거에요 -> GetArrowData 범위를 + return new ArrowData // 새 ArrowData를 만들어 반환할거에요 -> 화살 정보를 채워서 + { // 코드 블록을 시작할거에요 -> 초기화 블록 범위를 + arrowName = this.arrowName, // 값을 넣을거에요 -> 화살 이름을 + elementType = this.elementType, // 값을 넣을거에요 -> 속성 타입을 + baseDamage = this.baseDamage, // 값을 넣을거에요 -> 기본 데미지를 + elementDamage = this.elementDamage, // 값을 넣을거에요 -> 속성 데미지를 + elementDuration = this.elementDuration, // 값을 넣을거에요 -> 속성 지속시간을 + projectilePrefab = this.projectilePrefab // 값을 넣을거에요 -> 파티클 프리팹을 + }; // ArrowData 생성을 끝낼거에요 -> 반환 준비 완료 + } // 코드 블록을 끝낼거에요 -> GetArrowData를 - if (pickupCollider == null || physicsCollider == null) - { - AutoDetectColliders(); - } - } + private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + rb = GetComponent(); // 컴포넌트를 가져올거에요 -> Rigidbody를 찾아 rb에 저장 - /// - /// Collider 자동 탐지 (기존 로직 유지) - /// - private void AutoDetectColliders() - { - Collider[] allColliders = GetComponents(); + if (pickupCollider == null || physicsCollider == null) // 조건을 검사할거에요 -> 콜라이더 참조가 비어있는지 + { // 코드 블록을 시작할거에요 -> 자동 탐지 처리 + AutoDetectColliders(); // 함수를 실행할거에요 -> 콜라이더 자동 탐지 + } // 코드 블록을 끝낼거에요 -> 자동 탐지 처리 + } // 코드 블록을 끝낼거에요 -> Awake를 - foreach (Collider col in allColliders) - { - if (col is SphereCollider && pickupCollider == null) - { - pickupCollider = col; - Debug.Log($"습득용 Collider: {col.GetType().Name}"); - } - else if (col is BoxCollider && physicsCollider == null) - { - physicsCollider = col; - Debug.Log($"물리용 Collider: {col.GetType().Name}"); - } - } - } + /// // 요약 주석을 시작할거에요 -> 콜라이더 자동 탐지 설명을 + /// Collider 자동 탐지 (기존 로직 유지) // 설명할거에요 -> Sphere/Box를 찾아 자동 세팅 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + private void AutoDetectColliders() // 함수를 선언할거에요 -> 콜라이더를 자동으로 찾는 AutoDetectColliders를 + { // 코드 블록을 시작할거에요 -> AutoDetectColliders 범위를 + Collider[] allColliders = GetComponents(); // 배열을 가져올거에요 -> 내 오브젝트의 모든 Collider를 - private void Start() - { - SetupAsItem(); - } + foreach (Collider col in allColliders) // 반복할거에요 -> 모든 콜라이더를 하나씩 + { // 코드 블록을 시작할거에요 -> 콜라이더 판별 범위 + if (col is SphereCollider && pickupCollider == null) // 조건을 검사할거에요 -> SphereCollider이고 pickupCollider가 비어있는지 + { // 코드 블록을 시작할거에요 -> 습득용 콜라이더 설정 + pickupCollider = col; // 값을 넣을거에요 -> 습득용 콜라이더로 등록 + Debug.Log($"습득용 Collider: {col.GetType().Name}"); // 로그를 찍을거에요 -> 어떤 타입이 잡혔는지 + } // 코드 블록을 끝낼거에요 -> 습득용 설정 + else if (col is BoxCollider && physicsCollider == null) // 조건을 검사할거에요 -> BoxCollider이고 physicsCollider가 비어있는지 + { // 코드 블록을 시작할거에요 -> 물리용 콜라이더 설정 + physicsCollider = col; // 값을 넣을거에요 -> 물리용 콜라이더로 등록 + Debug.Log($"물리용 Collider: {col.GetType().Name}"); // 로그를 찍을거에요 -> 어떤 타입이 잡혔는지 + } // 코드 블록을 끝낼거에요 -> 물리용 설정 + } // 코드 블록을 끝낼거에요 -> 콜라이더 판별 + } // 코드 블록을 끝낼거에요 -> AutoDetectColliders를 - /// - /// 아이템 모드 설정 (기존 로직 유지 + 단순화) - /// - private void SetupAsItem() - { - GameObject player = GameObject.FindGameObjectWithTag("Player"); - if (player != null) - { - playerTransform = player.transform; - } + private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를 + { // 코드 블록을 시작할거에요 -> Start 범위를 + SetupAsItem(); // 함수를 실행할거에요 -> 아이템 모드로 세팅 + } // 코드 블록을 끝낼거에요 -> Start를 - if (pickupUI != null) pickupUI.SetActive(false); + /// // 요약 주석을 시작할거에요 -> 아이템 모드 세팅 설명을 + /// 아이템 모드 설정 (기존 로직 유지 + 단순화) // 설명할거에요 -> UI/콜라이더/물리 설정을 정리 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + private void SetupAsItem() // 함수를 선언할거에요 -> 아이템 상태로 세팅하는 SetupAsItem을 + { // 코드 블록을 시작할거에요 -> SetupAsItem 범위를 + GameObject player = GameObject.FindGameObjectWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그 오브젝트를 + if (player != null) // 조건을 검사할거에요 -> 플레이어를 찾았는지 + { // 코드 블록을 시작할거에요 -> 플레이어 참조 저장 + playerTransform = player.transform; // 값을 넣을거에요 -> 플레이어 위치를 저장 + } // 코드 블록을 끝낼거에요 -> 플레이어 참조 저장 - // Collider 설정 - if (pickupCollider != null) - { - pickupCollider.enabled = true; - pickupCollider.isTrigger = true; - } + if (pickupUI != null) pickupUI.SetActive(false); // 조건이 맞으면 실행할거에요 -> 시작 시 UI를 숨기기 - if (physicsCollider != null) - { - physicsCollider.enabled = true; - physicsCollider.isTrigger = false; - } + // Collider 설정 // 설명을 적을거에요 -> 습득용/물리용 콜라이더 모드 설정 + if (pickupCollider != null) // 조건을 검사할거에요 -> 습득용 콜라이더가 있는지 + { // 코드 블록을 시작할거에요 -> 습득용 콜라이더 설정 + pickupCollider.enabled = true; // 값을 바꿀거에요 -> 습득용 콜라이더 활성화 + pickupCollider.isTrigger = true; // 값을 바꿀거에요 -> 습득용은 트리거로 + } // 코드 블록을 끝낼거에요 -> 습득용 설정 - // 물리 설정 - if (rb != null) - { - rb.useGravity = true; - rb.isKinematic = false; - rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; - } - } + if (physicsCollider != null) // 조건을 검사할거에요 -> 물리용 콜라이더가 있는지 + { // 코드 블록을 시작할거에요 -> 물리용 콜라이더 설정 + physicsCollider.enabled = true; // 값을 바꿀거에요 -> 물리 콜라이더 활성화 + physicsCollider.isTrigger = false; // 값을 바꿀거에요 -> 물리용은 트리거 아님 + } // 코드 블록을 끝낼거에요 -> 물리용 설정 - private void Update() - { - if (playerTransform == null || pickupUI == null) return; + // 물리 설정 // 설명을 적을거에요 -> 리지드바디 세팅 + if (rb != null) // 조건을 검사할거에요 -> Rigidbody가 있는지 + { // 코드 블록을 시작할거에요 -> Rigidbody 설정 + rb.useGravity = true; // 값을 바꿀거에요 -> 중력 적용 + rb.isKinematic = false; // 값을 바꿀거에요 -> 물리 시뮬레이션 적용 + rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 값을 바꿀거에요 -> 빠른 충돌도 놓치지 않게 + } // 코드 블록을 끝낼거에요 -> Rigidbody 설정 + } // 코드 블록을 끝낼거에요 -> SetupAsItem을 - float distance = Vector3.Distance(transform.position, playerTransform.position); - pickupUI.SetActive(distance <= uiDisplayRange); - } + private void Update() // 함수를 선언할거에요 -> 매 프레임 실행되는 Update를 + { // 코드 블록을 시작할거에요 -> Update 범위를 + if (playerTransform == null || pickupUI == null) return; // 조건이 맞으면 종료할거에요 -> 플레이어/UI가 없으면 처리 불가 - /// - /// [MODIFIED] PlayerInteraction에서 'E' 키를 눌렀을 때 호출 - /// 기존: SwapArrow(gameObject) → 변경: SetCurrentArrow(ArrowData) - /// - public void Pickup(PlayerAttack playerAttack) - { - if (playerAttack == null) return; + float distance = Vector3.Distance(transform.position, playerTransform.position); // 값을 계산할거에요 -> 플레이어와의 거리를 + pickupUI.SetActive(distance <= uiDisplayRange); // 값을 적용할거에요 -> 거리 안이면 UI 켜고 밖이면 끄기 + } // 코드 블록을 끝낼거에요 -> Update를 - // ArrowData를 패키징하여 PlayerAttack에 전달 - ArrowData data = GetArrowData(); - playerAttack.SetCurrentArrow(data); + /// // 요약 주석을 시작할거에요 -> 습득 함수 설명을 + /// [MODIFIED] PlayerInteraction에서 'E' 키를 눌렀을 때 호출 // 설명할거에요 -> 상호작용으로 호출되는 함수 + /// 기존: SwapArrow(gameObject) → 변경: SetCurrentArrow(ArrowData) // 설명할거에요 -> 전달 방식 변경점 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + public void Pickup(PlayerAttack playerAttack) // 함수를 선언할거에요 -> 플레이어가 화살을 줍는 Pickup을 + { // 코드 블록을 시작할거에요 -> Pickup 범위를 + if (playerAttack == null) return; // 조건이 맞으면 종료할거에요 -> PlayerAttack이 없으면 진행 불가 - Debug.Log($"[{arrowName}] 화살 습득! 속성: {elementType}"); + ArrowData data = GetArrowData(); // 값을 만들거에요 -> 현재 화살 정보를 ArrowData로 패키징 + playerAttack.SetCurrentArrow(data); // 함수를 실행할거에요 -> 플레이어 공격 스크립트에 현재 화살로 세팅 - // 아이템 제거 - Destroy(gameObject); - } + Debug.Log($"[{arrowName}] 화살 습득! 속성: {elementType}"); // 로그를 찍을거에요 -> 습득/속성 확인 - /// - /// 몬스터 드롭 시 화살 정보 설정 (기존 함수 유지) - /// - public void SetArrowData(string name, float dmg, float spd, float rng) - { - arrowName = name; - baseDamage = dmg; - } + Destroy(gameObject); // 오브젝트를 삭제할거에요 -> 습득했으니 아이템 제거 + } // 코드 블록을 끝낼거에요 -> Pickup을 - /// - /// [NEW] 속성 정보까지 포함한 전체 세팅 함수 - /// - public void SetArrowDataFull( - string name, - ArrowElementType element, - float baseDmg, - float elemDmg, - float elemDur, - GameObject particlePrefab) - { - arrowName = name; - elementType = element; - baseDamage = baseDmg; - elementDamage = elemDmg; - elementDuration = elemDur; - projectilePrefab = particlePrefab; - } + /// // 요약 주석을 시작할거에요 -> 드롭 세팅 함수 설명을 + /// 몬스터 드롭 시 화살 정보 설정 (기존 함수 유지) // 설명할거에요 -> 예전 호환용 함수 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + public void SetArrowData(string name, float dmg, float spd, float rng) // 함수를 선언할거에요 -> 간단 세팅용 SetArrowData를 + { // 코드 블록을 시작할거에요 -> SetArrowData 범위를 + arrowName = name; // 값을 넣을거에요 -> 이름을 세팅 + baseDamage = dmg; // 값을 넣을거에요 -> 기본 데미지를 세팅 + } // 코드 블록을 끝낼거에요 -> SetArrowData를 - /// - /// 바닥 충돌 시 속도 감쇠 (기존 로직 유지) - /// - private void OnCollisionEnter(Collision collision) - { - if (rb != null && collision.gameObject.CompareTag("Ground")) - { - rb.velocity *= 0.5f; - rb.angularVelocity *= 0.5f; - } - } + /// // 요약 주석을 시작할거에요 -> 전체 세팅 함수 설명을 + /// [NEW] 속성 정보까지 포함한 전체 세팅 함수 // 설명할거에요 -> 속성/파티클까지 세팅하는 버전 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + public void SetArrowDataFull( // 함수를 선언할거에요 -> 화살 모든 정보를 세팅하는 SetArrowDataFull을 + string name, // 매개변수를 받을거에요 -> 화살 이름을 + ArrowElementType element, // 매개변수를 받을거에요 -> 속성 타입을 + float baseDmg, // 매개변수를 받을거에요 -> 기본 데미지를 + float elemDmg, // 매개변수를 받을거에요 -> 속성 데미지를 + float elemDur, // 매개변수를 받을거에요 -> 속성 지속 시간을 + GameObject particlePrefab) // 매개변수를 받을거에요 -> 파티클 프리팹을 + { // 코드 블록을 시작할거에요 -> SetArrowDataFull 범위를 + arrowName = name; // 값을 넣을거에요 -> 이름 세팅 + elementType = element; // 값을 넣을거에요 -> 속성 세팅 + baseDamage = baseDmg; // 값을 넣을거에요 -> 기본 데미지 세팅 + elementDamage = elemDmg; // 값을 넣을거에요 -> 속성 데미지 세팅 + elementDuration = elemDur; // 값을 넣을거에요 -> 속성 지속시간 세팅 + projectilePrefab = particlePrefab; // 값을 넣을거에요 -> 파티클 프리팹 세팅 + } // 코드 블록을 끝낼거에요 -> SetArrowDataFull을 - #region 디버그 시각화 + /// // 요약 주석을 시작할거에요 -> 바닥 충돌 감쇠 설명을 + /// 바닥 충돌 시 속도 감쇠 (기존 로직 유지) // 설명할거에요 -> 바닥에 튕김을 줄이기 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + private void OnCollisionEnter(Collision collision) // 함수를 선언할거에요 -> 충돌이 발생하면 호출되는 OnCollisionEnter를 + { // 코드 블록을 시작할거에요 -> OnCollisionEnter 범위를 + if (rb != null && collision.gameObject.CompareTag("Ground")) // 조건을 검사할거에요 -> Rigidbody가 있고 땅에 닿았는지 + { // 코드 블록을 시작할거에요 -> 감쇠 처리 + rb.velocity *= 0.5f; // 값을 줄일거에요 -> 선속도를 절반으로 + rb.angularVelocity *= 0.5f; // 값을 줄일거에요 -> 회전속도를 절반으로 + } // 코드 블록을 끝낼거에요 -> 감쇠 처리 + } // 코드 블록을 끝낼거에요 -> OnCollisionEnter를 - private void OnDrawGizmosSelected() - { - if (pickupCollider != null && pickupCollider.enabled) - { - Gizmos.color = Color.green; - if (pickupCollider is SphereCollider sphere) - { - Gizmos.DrawWireSphere(transform.position + sphere.center, sphere.radius); - } - } + #region 디버그 시각화 // 영역을 표시할거에요 -> Gizmos 관련 코드 묶음임을 - if (physicsCollider != null && physicsCollider.enabled) - { - Gizmos.color = Color.blue; - if (physicsCollider is BoxCollider box) - { - Gizmos.matrix = transform.localToWorldMatrix; - Gizmos.DrawWireCube(box.center, box.size); - } - } + private void OnDrawGizmosSelected() // 함수를 선언할거에요 -> 선택됐을 때 Gizmo를 그리는 OnDrawGizmosSelected를 + { // 코드 블록을 시작할거에요 -> OnDrawGizmosSelected 범위를 + if (pickupCollider != null && pickupCollider.enabled) // 조건을 검사할거에요 -> 습득용 콜라이더가 있고 켜져있는지 + { // 코드 블록을 시작할거에요 -> 습득 범위 시각화 + Gizmos.color = Color.green; // 색을 정할거에요 -> 초록색으로 + if (pickupCollider is SphereCollider sphere) // 조건을 검사할거에요 -> 습득용이 SphereCollider인지 + { // 코드 블록을 시작할거에요 -> 구체 범위 그리기 + Gizmos.DrawWireSphere(transform.position + sphere.center, sphere.radius); // 선 구체를 그릴거에요 -> 습득 범위를 + } // 코드 블록을 끝낼거에요 -> 구체 그리기 + } // 코드 블록을 끝낼거에요 -> 습득 범위 시각화 - // [NEW] 속성 색상 표시 - switch (elementType) - { - case ArrowElementType.Fire: Gizmos.color = Color.red; break; - case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; - case ArrowElementType.Poison: Gizmos.color = new Color(0.5f, 1f, 0f); break; - case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; - default: Gizmos.color = Color.white; break; - } - Gizmos.DrawWireSphere(transform.position, 0.3f); - } + if (physicsCollider != null && physicsCollider.enabled) // 조건을 검사할거에요 -> 물리 콜라이더가 있고 켜져있는지 + { // 코드 블록을 시작할거에요 -> 물리 범위 시각화 + Gizmos.color = Color.blue; // 색을 정할거에요 -> 파란색으로 + if (physicsCollider is BoxCollider box) // 조건을 검사할거에요 -> 물리용이 BoxCollider인지 + { // 코드 블록을 시작할거에요 -> 박스 범위 그리기 + Gizmos.matrix = transform.localToWorldMatrix; // 좌표계를 바꿀거에요 -> 오브젝트 로컬 기준으로 + Gizmos.DrawWireCube(box.center, box.size); // 선 박스를 그릴거에요 -> 물리 충돌 범위를 + } // 코드 블록을 끝낼거에요 -> 박스 그리기 + } // 코드 블록을 끝낼거에요 -> 물리 범위 시각화 - #endregion -} \ No newline at end of file + // [NEW] 속성 색상 표시 // 설명을 적을거에요 -> 속성 타입에 따라 색을 다르게 + switch (elementType) // 분기할거에요 -> elementType에 따라 + { // 코드 블록을 시작할거에요 -> switch 범위를 + case ArrowElementType.Fire: Gizmos.color = Color.red; break; // 빨강으로 칠할거에요 -> 불 속성이면 + case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; // 하늘색으로 칠할거에요 -> 얼음 속성이면 + case ArrowElementType.Poison: Gizmos.color = new Color(0.5f, 1f, 0f); break; // 연두로 칠할거에요 -> 독 속성이면 + case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; // 노랑으로 칠할거에요 -> 번개 속성이면 + default: Gizmos.color = Color.white; break; // 흰색으로 칠할거에요 -> 기본/없음이면 + } // 코드 블록을 끝낼거에요 -> switch를 + + Gizmos.DrawWireSphere(transform.position, 0.3f); // 선 구체를 그릴거에요 -> 아이템 위치에 속성 표시용으로 + } // 코드 블록을 끝낼거에요 -> OnDrawGizmosSelected를 + + #endregion // 영역을 끝낼거에요 -> Gizmos 묶음을 + +} // 코드 블록을 끝낼거에요 -> ArrowPickup을 \ No newline at end of file diff --git a/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs b/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs index 44cb5543..d800971f 100644 --- a/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs +++ b/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs @@ -1,43 +1,42 @@ -using System.Collections.Generic; -using UnityEngine; +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 /// /// 플레이어의 전투 행동을 슬라이딩 윈도우(최근 N초) 기반으로 추적합니다. -/// 보스 AI가 이 데이터를 읽어 카운터 패턴 발동 여부를 판단합니다. /// -public class PlayerBehaviorTracker : MonoBehaviour +public class PlayerBehaviorTracker : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerBehaviorTracker를 { - public static PlayerBehaviorTracker Instance { get; private set; } + public static PlayerBehaviorTracker Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를 - [Header("슬라이딩 윈도우 설정")] - [Tooltip("행동 추적 윈도우 크기(초)")] - [SerializeField] private float windowDuration = 10f; + [Header("슬라이딩 윈도우 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 슬라이딩 윈도우 설정 을 + [Tooltip("행동 추적 윈도우 크기(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float windowDuration = 10f; // 변수를 선언할거에요 -> 추적 시간 범위(10초)를 windowDuration에 // ── 내부 기록용 타임스탬프 리스트 ── - private List dodgeTimestamps = new List(); - private List aimStartTimes = new List(); - private List aimEndTimes = new List(); - private List pierceShotTimestamps = new List(); - private List totalShotTimestamps = new List(); + private List dodgeTimestamps = new List(); // 리스트를 만들거에요 -> 회피 시간들을 저장할 dodgeTimestamps를 + private List aimStartTimes = new List(); // 리스트를 만들거에요 -> 조준 시작 시간들을 저장할 aimStartTimes를 + private List aimEndTimes = new List(); // 리스트를 만들거에요 -> 조준 종료 시간들을 저장할 aimEndTimes를 + private List pierceShotTimestamps = new List(); // 리스트를 만들거에요 -> 관통샷 시간들을 저장할 pierceShotTimestamps를 + private List totalShotTimestamps = new List(); // 리스트를 만들거에요 -> 전체 사격 시간들을 저장할 totalShotTimestamps를 // ── 현재 조준 상태 ── - private bool isAiming = false; - private float currentAimStartTime; + private bool isAiming = false; // 변수를 초기화할거에요 -> 조준 중 여부를 거짓(false)으로 + private float currentAimStartTime; // 변수를 선언할거에요 -> 현재 조준 시작 시간을 저장할 currentAimStartTime을 // ── 외부에서 읽는 프로퍼티 ── - public int DodgeCount => CountInWindow(dodgeTimestamps); - public float AimHoldTime => CalculateAimHoldTime(); - public float PierceRatio => CalculatePierceRatio(); - public int TotalShotsInWindow => CountInWindow(totalShotTimestamps); + public int DodgeCount => CountInWindow(dodgeTimestamps); // 값을 반환할거에요 -> 최근 회피 횟수를 계산해서 + public float AimHoldTime => CalculateAimHoldTime(); // 값을 반환할거에요 -> 최근 조준 유지 시간을 계산해서 + public float PierceRatio => CalculatePierceRatio(); // 값을 반환할거에요 -> 최근 관통샷 비율을 계산해서 + public int TotalShotsInWindow => CountInWindow(totalShotTimestamps); // 값을 반환할거에요 -> 최근 총 발사 횟수를 계산해서 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를 { - if (Instance != null && Instance != this) + if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면 { - Destroy(gameObject); - return; + Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를 + return; // 중단할거에요 -> 초기화 로직을 } - Instance = this; + Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에 } // ═══════════════════════════════════════════ @@ -45,39 +44,39 @@ public class PlayerBehaviorTracker : MonoBehaviour // ═══════════════════════════════════════════ /// 플레이어가 회피(대시/구르기)를 수행했을 때 호출 - public void RecordDodge() + public void RecordDodge() // 함수를 선언할거에요 -> 회피를 기록하는 RecordDodge를 { - dodgeTimestamps.Add(Time.time); + dodgeTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 회피 목록에 } /// 플레이어가 조준을 시작했을 때 호출 - public void RecordAimStart() + public void RecordAimStart() // 함수를 선언할거에요 -> 조준 시작을 기록하는 RecordAimStart를 { - if (!isAiming) + if (!isAiming) // 조건이 맞으면 실행할거에요 -> 이미 조준 중이 아니라면 { - isAiming = true; - currentAimStartTime = Time.time; + isAiming = true; // 상태를 바꿀거에요 -> 조준 중 상태를 참(true)으로 + currentAimStartTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 조준 시작 시간으로 } } /// 플레이어가 조준을 해제했을 때 호출 - public void RecordAimEnd() + public void RecordAimEnd() // 함수를 선언할거에요 -> 조준 종료를 기록하는 RecordAimEnd를 { - if (isAiming) + if (isAiming) // 조건이 맞으면 실행할거에요 -> 조준 중이었다면 { - isAiming = false; - aimStartTimes.Add(currentAimStartTime); - aimEndTimes.Add(Time.time); + isAiming = false; // 상태를 바꿀거에요 -> 조준 중 상태를 거짓(false)으로 + aimStartTimes.Add(currentAimStartTime); // 추가할거에요 -> 시작 시간을 리스트에 + aimEndTimes.Add(Time.time); // 추가할거에요 -> 종료(현재) 시간을 리스트에 } } /// 화살 발사 시 호출. isPierce=true면 관통 화살 - public void RecordShot(bool isPierce) + public void RecordShot(bool isPierce) // 함수를 선언할거에요 -> 발사를 기록하는 RecordShot을 { - totalShotTimestamps.Add(Time.time); - if (isPierce) + totalShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 전체 사격 목록에 + if (isPierce) // 조건이 맞으면 실행할거에요 -> 관통 화살이라면 { - pierceShotTimestamps.Add(Time.time); + pierceShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 관통 사격 목록에 } } @@ -86,62 +85,61 @@ public class PlayerBehaviorTracker : MonoBehaviour // ═══════════════════════════════════════════ /// 새 런 시작 시 모든 행동 기록을 초기화 - public void ResetForNewRun() + public void ResetForNewRun() // 함수를 선언할거에요 -> 모든 기록을 초기화하는 ResetForNewRun을 { - dodgeTimestamps.Clear(); - aimStartTimes.Clear(); - aimEndTimes.Clear(); - pierceShotTimestamps.Clear(); - totalShotTimestamps.Clear(); - isAiming = false; + dodgeTimestamps.Clear(); // 비울거에요 -> 회피 기록을 + aimStartTimes.Clear(); // 비울거에요 -> 조준 시작 기록을 + aimEndTimes.Clear(); // 비울거에요 -> 조준 종료 기록을 + pierceShotTimestamps.Clear(); // 비울거에요 -> 관통 사격 기록을 + totalShotTimestamps.Clear(); // 비울거에요 -> 전체 사격 기록을 + isAiming = false; // 초기화할거에요 -> 조준 상태를 거짓(false)으로 } // ═══════════════════════════════════════════ // 내부 계산 // ═══════════════════════════════════════════ - private int CountInWindow(List timestamps) + private int CountInWindow(List timestamps) // 함수를 선언할거에요 -> 윈도우 내 개수를 세는 CountInWindow를 { - float cutoff = Time.time - windowDuration; - // 오래된 항목 제거 - timestamps.RemoveAll(t => t < cutoff); - return timestamps.Count; + float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 현재 시간에서 윈도우 크기를 뺀 기준 시간을 + timestamps.RemoveAll(t => t < cutoff); // 삭제할거에요 -> 기준 시간보다 오래된 기록들을 리스트에서 + return timestamps.Count; // 반환할거에요 -> 남은 기록의 개수를 } - private float CalculateAimHoldTime() + private float CalculateAimHoldTime() // 함수를 선언할거에요 -> 총 조준 시간을 계산하는 CalculateAimHoldTime을 { - float cutoff = Time.time - windowDuration; - float total = 0f; + float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 기준 시간(현재 - 윈도우)을 + float total = 0f; // 변수를 초기화할거에요 -> 총 시간을 0으로 // 완료된 조준 세션 - for (int i = aimStartTimes.Count - 1; i >= 0; i--) + for (int i = aimStartTimes.Count - 1; i >= 0; i--) // 반복할거에요 -> 저장된 조준 기록들을 역순으로 { - if (aimEndTimes[i] < cutoff) + if (aimEndTimes[i] < cutoff) // 조건이 맞으면 실행할거에요 -> 종료 시간이 기준보다 옛날이라면 { - aimStartTimes.RemoveAt(i); - aimEndTimes.RemoveAt(i); - continue; + aimStartTimes.RemoveAt(i); // 삭제할거에요 -> 해당 시작 기록을 + aimEndTimes.RemoveAt(i); // 삭제할거에요 -> 해당 종료 기록을 + continue; // 건너뛸거에요 -> 다음 반복으로 } - float start = Mathf.Max(aimStartTimes[i], cutoff); - total += aimEndTimes[i] - start; + float start = Mathf.Max(aimStartTimes[i], cutoff); // 값을 계산할거에요 -> 시작 시간과 기준 시간 중 더 최근 것을 + total += aimEndTimes[i] - start; // 값을 더할거에요 -> 유효한 조준 기간을 총 시간에 } // 현재 조준 중인 시간도 포함 - if (isAiming) + if (isAiming) // 조건이 맞으면 실행할거에요 -> 현재 조준 중이라면 { - float start = Mathf.Max(currentAimStartTime, cutoff); - total += Time.time - start; + float start = Mathf.Max(currentAimStartTime, cutoff); // 값을 계산할거에요 -> 조준 시작과 기준 중 최근 것을 + total += Time.time - start; // 값을 더할거에요 -> 지금까지의 조준 시간을 총 시간에 } - return total; + return total; // 반환할거에요 -> 계산된 총 조준 시간을 } - private float CalculatePierceRatio() + private float CalculatePierceRatio() // 함수를 선언할거에요 -> 관통 비율을 계산하는 CalculatePierceRatio를 { - int totalShots = CountInWindow(totalShotTimestamps); - if (totalShots == 0) return 0f; + int totalShots = CountInWindow(totalShotTimestamps); // 값을 가져올거에요 -> 최근 총 발사 횟수를 + if (totalShots == 0) return 0f; // 조건이 맞으면 반환할거에요 -> 발사 기록이 없으면 0을 - int pierceShots = CountInWindow(pierceShotTimestamps); - return (float)pierceShots / totalShots; + int pierceShots = CountInWindow(pierceShotTimestamps); // 값을 가져올거에요 -> 최근 관통 발사 횟수를 + return (float)pierceShots / totalShots; // 반환할거에요 -> 관통 횟수 나누기 전체 횟수(비율)를 } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Player/Controller/PlayerInput.cs b/Assets/Scripts/Player/Controller/PlayerInput.cs index 2d3da6fc..6e517429 100644 --- a/Assets/Scripts/Player/Controller/PlayerInput.cs +++ b/Assets/Scripts/Player/Controller/PlayerInput.cs @@ -1,67 +1,67 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class PlayerInput : MonoBehaviour +public class PlayerInput : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerInput을 { // 다른 스크립트에게 명령을 내려야 하니, 미리 참조 시킴 - [SerializeField] private PlayerHealth health; // 죽었는지 확인용 - [SerializeField] private PlayerMovement movement; // 이동 명령용 - [SerializeField] private PlayerAim aim; // 회전 명령용 - [SerializeField] private PlayerInteraction interaction; // 아이템 줍기 명령용 - [SerializeField] private PlayerAttack attack; - [SerializeField] private PlayerStatsUI statsUI; // UI 창 열기 명령용 + [SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 죽었는지 확인할 health를 + [SerializeField] private PlayerMovement movement; // 변수를 선언할거에요 -> 이동 명령을 내릴 movement를 + [SerializeField] private PlayerAim aim; // 변수를 선언할거에요 -> 회전 명령을 내릴 aim을 + [SerializeField] private PlayerInteraction interaction; // 변수를 선언할거에요 -> 상호작용 명령을 내릴 interaction을 + [SerializeField] private PlayerAttack attack; // 변수를 선언할거에요 -> 공격 명령을 내릴 attack을 + [SerializeField] private PlayerStatsUI statsUI; // 변수를 선언할거에요 -> UI 토글을 위한 statsUI를 - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를 { // 1. 사망 체크 // 죽었는데 키보드 눌린다고 시체가 움직이면 안 되니까 아예 입력을 차단(return)함 - if (health != null && health.IsDead) return; + if (health != null && health.IsDead) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 죽었다면 // 2. UI 토글 (C키) // 스탯 창을 껐다 켰다 함 - if (Input.GetKeyDown(KeyCode.C) && statsUI != null) statsUI.ToggleWindow(); + if (Input.GetKeyDown(KeyCode.C) && statsUI != null) statsUI.ToggleWindow(); // 조건이 맞으면 실행할거에요 -> C키를 눌렀다면 스탯창 토글을 // 3. 이동 입력 감지 // GetAxisRaw를 쓴 이유: 0에서 1로 부드럽게 변하는 게 아니라, // 키를 누르면 즉시 1, 떼면 0이 되어서 빠릿빠릿한 조작감을 줌 - float h = Input.GetAxisRaw("Horizontal"); // A, D 키 (-1, 0, 1) - float v = Input.GetAxisRaw("Vertical"); // W, S 키 (-1, 0, 1) - bool sprint = Input.GetKey(KeyCode.LeftShift); // Shift 누르고 있나? + float h = Input.GetAxisRaw("Horizontal"); // 값을 가져올거에요 -> 좌우 입력값(-1, 0, 1)을 h에 + float v = Input.GetAxisRaw("Vertical"); // 값을 가져올거에요 -> 상하 입력값(-1, 0, 1)을 v에 + bool sprint = Input.GetKey(KeyCode.LeftShift); // 값을 가져올거에요 -> 쉬프트 키 입력 여부를 sprint에 // 4. 이동 명령 하달 - if (movement != null) + if (movement != null) // 조건이 맞으면 실행할거에요 -> 이동 스크립트가 있다면 { // 방향 벡터를 정규화(.normalized)해서 대각선 이동 시 빨라지는 걸 방지 // 그리고 달리기 여부(sprint)도 같이 넘겨줍니다. - movement.SetMoveInput(new Vector3(h, 0, v).normalized, sprint); + movement.SetMoveInput(new Vector3(h, 0, v).normalized, sprint); // 실행할거에요 -> 이동 입력 전달 함수를 // ⭐ [추가] 스페이스바 누르면 대시 명령! - if (Input.GetKeyDown(KeyCode.Space)) movement.AttemptDash(); + if (Input.GetKeyDown(KeyCode.Space)) movement.AttemptDash(); // 조건이 맞으면 실행할거에요 -> 스페이스바 누르면 대시 시도를 } // 5. 마우스 회전 명령 // 캐릭터가 마우스 커서를 바라보게 합니다. - if (aim != null) aim.RotateTowardsMouse(); + if (aim != null) aim.RotateTowardsMouse(); // 조건이 맞으면 실행할거에요 -> 마우스 방향 회전 함수를 // 6. 상호작용 (F키) // 바닥에 떨어진 무기를 줍기 - if (Input.GetKeyDown(KeyCode.F) && interaction != null) interaction.TryInteract(); + if (Input.GetKeyDown(KeyCode.F) && interaction != null) interaction.TryInteract(); // 조건이 맞으면 실행할거에요 -> F키 누르면 상호작용 시도를 // 7. 공격 입력 (좌클릭/우클릭) - if (attack != null) + if (attack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면 { // 우클릭 꾹: 차징(모으기) 시작 - if (Input.GetMouseButtonDown(1)) attack.StartCharging(); + if (Input.GetMouseButtonDown(1)) attack.StartCharging(); // 조건이 맞으면 실행할거에요 -> 우클릭 누르면 차징 시작을 // 우클릭 뗌: 차징 취소 (공격 안 하고 캔슬) - if (Input.GetMouseButtonUp(1)) attack.CancelCharging(); + if (Input.GetMouseButtonUp(1)) attack.CancelCharging(); // 조건이 맞으면 실행할거에요 -> 우클릭 떼면 차징 취소를 // 좌클릭: 공격 시도 - if (Input.GetMouseButtonDown(0)) + if (Input.GetMouseButtonDown(0)) // 조건이 맞으면 실행할거에요 -> 좌클릭을 눌렀다면 { // 만약 우클릭(방어/조준) 상태에서 좌클릭을 했다면? -> 투척(Release) - if (Input.GetMouseButton(1)) attack.ReleaseAttack(); + if (Input.GetMouseButton(1)) attack.ReleaseAttack(); // 조건이 맞으면 실행할거에요 -> 차징 중이라면 발사를 // 그냥 좌클릭만 했다면? -> 일반 평타(Normal Attack) - else attack.PerformNormalAttack(); + else attack.PerformNormalAttack(); // 그 외엔 실행할거에요 -> 일반 공격을 } } } diff --git a/Assets/Scripts/Player/Controller/PlayerMovement.cs b/Assets/Scripts/Player/Controller/PlayerMovement.cs index e70f7fbf..61bd33fa 100644 --- a/Assets/Scripts/Player/Controller/PlayerMovement.cs +++ b/Assets/Scripts/Player/Controller/PlayerMovement.cs @@ -1,277 +1,216 @@ -using UnityEngine; -using System.Collections; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 - -[RequireComponent(typeof(CharacterController))] // 이 스크립트를 넣으면 CharacterController도 자동으로 추가됨 (실수 방지) -public class PlayerMovement : MonoBehaviour +[RequireComponent(typeof(CharacterController))] // 컴포넌트를 강제로 추가할거에요 -> CharacterController를 (이 스크립트를 넣으면 자동으로) +public class PlayerMovement : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerMovement를 { - [Header("=== 참조 ===")] - // 외부 스크립트들을 상속 시키기 위한 변수들 - // 'private'에 [SerializeField]를 붙여 에디터 인스펙터 창에서 설정 가능 - [SerializeField] private Stats stats; // 이동 속도(Stat)를 가져오기 위해 필요 - [SerializeField] private PlayerHealth health; // 죽거나 맞았을 때 이동을 멈추기 위해 필요 - [SerializeField] private PlayerAnimator pAnim; // 이동에 맞춰 뛰는 모션을 재생하기 위해 필요 - [SerializeField] private PlayerAttack attackScript; // 공격 중일 때 이동을 막기 위해 필요 + [Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 참조 === 를 - [Header("=== CharacterController ===")] - //CharacterControlle는 유니티가 제공하는 인간형 이동 컴포넌트 - // Rigidbody와 달리 경사면, 계단 처리가 자동으로 됨. - private CharacterController _controller; + [SerializeField] private Stats stats; // 변수를 선언하고 인스펙터에 노출할거에요 -> 이동 속도 정보를 가진 Stats 스크립트를 + [SerializeField] private PlayerHealth health; // 변수를 선언하고 인스펙터에 노출할거에요 -> 사망 및 피격 상태를 알기 위한 PlayerHealth를 + [SerializeField] private PlayerAnimator pAnim; // 변수를 선언하고 인스펙터에 노출할거에요 -> 애니메이션을 제어할 PlayerAnimator를 + [SerializeField] private PlayerAttack attackScript; // 변수를 선언하고 인스펙터에 노출할거에요 -> 공격 상태를 알기 위한 PlayerAttack을 - [Header("=== 대시 설정 ===")] - [SerializeField] private float dashDistance = 3f; // 대시로 이동할 총 거리 - [SerializeField] private float dashDuration = 0.08f; // 대시가 끝나는 시간 (짧을수록 빠름) - [SerializeField] private float dashCooldown = 1.5f; // 대시 후 다시 쓰기까지 기다리는 시간 + [Header("=== CharacterController ===")] // 인스펙터 창에 제목을 표시할거에요 -> === CharacterController === 를 + private CharacterController _controller; // 변수를 선언할거에요 -> 이동을 담당할 CharacterController 컴포넌트를 담을 _controller를 - [Header("=== 차징 감속 설정 ===")] - [Range(0.1f, 1f)] // 슬라이더로 조절하게 만듦 (1이면 감속 없음, 0.1이면 엄청 느려짐) - [SerializeField] private float minSpeedMultiplier = 0.3f; // 차징 중일 때 속도를 30%로 줄임 + [Header("=== 대시 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 대시 설정 === 을 + [SerializeField] private float dashDistance = 3f; // 변수를 선언할거에요 -> 대시로 이동할 거리(3.0)를 dashDistance에 + [SerializeField] private float dashDuration = 0.08f; // 변수를 선언할거에요 -> 대시가 지속될 시간(0.08초)을 dashDuration에 + [SerializeField] private float dashCooldown = 1.5f; // 변수를 선언할거에요 -> 대시 재사용 대기시간(1.5초)을 dashCooldown에 - [Header("=== 중력 설정 ===")] - [SerializeField] private float gravity = -20f; // 지구 중력(-9.81)보다 세게 해서 점프 후 빨리 떨어지게 함 (조작감 향상) + [Header("=== 차징 감속 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 차징 감속 설정 === 을 + [Range(0.1f, 1f)] // 슬라이더를 만들거에요 -> 0.1부터 1 사이의 값으로 조절하는 + [SerializeField] private float minSpeedMultiplier = 0.3f; // 변수를 선언할거에요 -> 차징 중 속도 배율(30%)을 minSpeedMultiplier에 - [Header("=== 충돌 설정 ===")] - [Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")] - [SerializeField] private LayerMask weaponLayer; // 무기를 밟고 올라가는 버그를 막기 위해 무기 레이어를 지정 + [Header("=== 중력 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 중력 설정 === 을 + [SerializeField] private float gravity = -20f; // 변수를 선언할거에요 -> 중력 가속도(-20)를 gravity에 - //[Tooltip("플레이어가 서 있어야 하는 최소 높이 (버그 방지)")] - //[SerializeField] private float minGroundHeight = 0.1f; // 땅 보정 시 바닥에 파묻히지 않게 살짝 띄워주는 값 + [Header("=== 충돌 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 충돌 설정 === 을 + [Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private LayerMask weaponLayer; // 변수를 선언할거에요 -> 충돌을 무시할 무기 레이어를 weaponLayer에 // 내부 상태 변수 (로직 계산용) - private Vector3 _moveDir; // 키보드 입력(WASD)으로 결정된 이동 방향 - private bool _isSprinting; // Shift 키를 눌렀는지 여부 - private bool _isDashing; // 지금 대시 중인가? (중복 대시 방지) - private float _lastDashTime; // 마지막으로 대시를 쓴 시간 (쿨타임 계산용) - private float _verticalVelocity; // 수직 속도 (중력 가속도 계산용) + private Vector3 _moveDir; // 변수를 선언할거에요 -> 입력받은 이동 방향을 저장할 _moveDir을 + private bool _isSprinting; // 변수를 선언할거에요 -> 달리기 중인지 여부를 저장할 _isSprinting을 + private bool _isDashing; // 변수를 선언할거에요 -> 현재 대시 중인지 여부를 저장할 _isDashing을 + private float _lastDashTime; // 변수를 선언할거에요 -> 마지막으로 대시를 쓴 시간을 저장할 _lastDashTime을 + private float _verticalVelocity; // 변수를 선언할거에요 -> 수직(낙하) 속도를 저장할 _verticalVelocity를 // 디버그용 (초기 위치 저장) - private float _initialYPosition; + private float _initialYPosition; // 변수를 선언할거에요 -> 시작 높이를 저장할 _initialYPosition을 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를 { - // 내 몸에 붙어있는 CharacterController를 가져옴 - _controller = GetComponent(); + _controller = GetComponent(); // 컴포넌트를 가져와서 저장할거에요 -> 내 몸에 있는 CharacterController를 _controller에 - // 만약 없다면 콘솔창에 에러를 띄움 - if (_controller == null) + if (_controller == null) // 조건이 맞으면 실행할거에요 -> 컨트롤러를 찾지 못했다면 { - Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); + Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); // 에러 로그를 띄울거에요 -> 컴포넌트가 필요하다는 메시지를 } - _initialYPosition = transform.position.y; + _initialYPosition = transform.position.y; // 값을 저장할거에요 -> 현재의 Y축 위치를 _initialYPosition에 } - private void Start() + private void Start() // 함수를 실행할거에요 -> 첫 프레임 시작 전에 호출되는 Start를 { - // 게임 시작 시 딱 한 번 실행: 무기와 플레이어끼리 충돌 끄기 - SetupLayerCollision(); + SetupLayerCollision(); // 함수를 실행할거에요 -> 레이어 충돌 설정을 담당하는 SetupLayerCollision을 } - /// 레이어 충돌 무시 설정 (플레이어 자살 버그 방지) - - private void SetupLayerCollision() + private void SetupLayerCollision() // 함수를 선언할거에요 -> 레이어 충돌을 설정하는 SetupLayerCollision을 { - int playerLayer = gameObject.layer; // 내 레이어 번호 가져오기 + int playerLayer = gameObject.layer; // 값을 가져올거에요 -> 내 게임 오브젝트의 레이어 번호를 playerLayer에 - // 무기 레이어가 설정되어 있다면 - if (weaponLayer != 0) + if (weaponLayer != 0) // 조건이 맞으면 실행할거에요 -> 무기 레이어가 설정되어 있다면(0이 아니라면) { - // 비트 연산으로 되어있는 LayerMask에서 숫자 인덱스를 뽑음 - int weaponLayerIndex = GetLayerFromMask(weaponLayer); + int weaponLayerIndex = GetLayerFromMask(weaponLayer); // 함수를 실행해서 값을 받을거에요 -> 비트마스크를 정수 인덱스로 변환해서 weaponLayerIndex에 - // 유효한 레이어라면 물리 엔진에게 충돌하지 말라고 명령. - - if (weaponLayerIndex >= 0) + if (weaponLayerIndex >= 0) // 조건이 맞으면 실행할거에요 -> 유효한 레이어 인덱스(0 이상)라면 { - Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true); - Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료"); + Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true); // 설정을 변경할거에요 -> 플레이어와 무기 레이어 간의 충돌을 무시하도록(true) + Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료"); // 로그를 출력할거에요 -> 충돌 무시 설정이 완료되었다는 메시지를 } } } // 비트 마스크(이진수)를 정수 인덱스로 변환하는 수학 함수 - private int GetLayerFromMask(LayerMask mask) + private int GetLayerFromMask(LayerMask mask) // 함수를 선언할거에요 -> 마스크를 인덱스로 바꾸는 GetLayerFromMask를 { - int layerNumber = 0; - int layer = mask.value; - while (layer > 1) + int layerNumber = 0; // 변수를 초기화할거에요 -> 레이어 번호를 셀 layerNumber를 0으로 + int layer = mask.value; // 값을 가져올거에요 -> 마스크의 실제 정수값을 layer에 + while (layer > 1) // 반복할거에요 -> layer 값이 1보다 클 때까지 { - layer = layer >> 1; // 비트를 오른쪽으로 밀면서 횟수를 셉니다. - layerNumber++; + layer = layer >> 1; // 비트 연산을 할거에요 -> 비트를 오른쪽으로 한 칸 밀어서(나누기 2) + layerNumber++; // 값을 증가시킬거에요 -> 레이어 번호 카운트를 1만큼 } - return layerNumber; + return layerNumber; // 값을 반환할거에요 -> 계산된 레이어 번호를 } // 외부(InputHandler)에서 키보드 입력을 넣어주는 함수 - public void SetMoveInput(Vector3 dir, bool sprint) + public void SetMoveInput(Vector3 dir, bool sprint) // 함수를 선언할거에요 -> 이동 입력을 받아오는 SetMoveInput을 { - _moveDir = dir; // 방향 저장 - _isSprinting = sprint; // 달리기 여부 저장 + _moveDir = dir; // 값을 저장할거에요 -> 입력받은 방향(dir)을 _moveDir에 + _isSprinting = sprint; // 값을 저장할거에요 -> 달리기 여부(sprint)를 _isSprinting에 } - public void AttemptDash() + public void AttemptDash() // 함수를 선언할거에요 -> 대시를 시도하는 AttemptDash를 { - // 쿨타임, 생존 여부 등을 체크하고 통과하면 대시 시작 - if (CanDash()) StartCoroutine(DashRoutine()); + if (CanDash()) StartCoroutine(DashRoutine()); // 조건이 맞으면 실행할거에요 -> 대시가 가능하다면(CanDash) 대시 코루틴(DashRoutine)을 } - private bool CanDash() + private bool CanDash() // 함수를 선언할거에요 -> 대시 가능 여부를 판단하는 CanDash를 { - // 현재 시간 > 마지막 대시 시간 + 쿨타임 (즉, 쿨타임 지남) - bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown; - // 체력 스크립트가 있다면 살아있는지 확인 - bool isAlive = health != null && !health.IsDead; - // 쿨타임 끝남 && 대시 중 아님 && 살아있음 -> true - return isCooldownOver && !_isDashing && isAlive; + bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown; // 조건을 검사할거에요 -> 현재 시간이 쿨타임 이후인지 확인해서 isCooldownOver에 + bool isAlive = health != null && !health.IsDead; // 조건을 검사할거에요 -> 체력 스크립트가 있고 살아있는지 확인해서 isAlive에 + return isCooldownOver && !_isDashing && isAlive; // 결과를 반환할거에요 -> 쿨타임 끝남, 대시 안 함, 살아있음이 모두 참일 때만 true를 } - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 { // 1. 행동 불가 상태 체크 (가장 먼저 해서 불필요한 연산 방지) - // 죽었거나, 맞았거나, 대시 중이면 키보드 이동을 막습니다. - if (health != null && (health.IsDead || health.isHit || _isDashing)) + if (health != null && (health.IsDead || health.isHit || _isDashing)) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있고 (죽었거나, 맞았거나, 대시 중이라면) { - if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f); // 애니메이션 멈춤 - ApplyGravityOnly(); // 이동은 못 해도 중력은 받아야 바닥으로 떨어짐 - return; + if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있고 죽지 않았다면 이동 모션을 0(정지)으로 + ApplyGravityOnly(); // 함수를 실행할거에요 -> 이동은 안 해도 중력은 적용하는 ApplyGravityOnly를 + return; // 중단할거에요 -> 더 이상 움직임 코드를 실행하지 않도록 함수를 } // 2. 공격 중 이동 차단 - // 공격 모션 중에 미끄러지듯 이동하는 '스케이트 현상' 방지 - if (attackScript != null && attackScript.IsAttacking) + if (attackScript != null && attackScript.IsAttacking) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있고 공격 중이라면 { - if (pAnim != null) pAnim.UpdateMove(0f); - ApplyGravityOnly(); - return; + if (pAnim != null) pAnim.UpdateMove(0f); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 이동 모션을 0(정지)으로 + ApplyGravityOnly(); // 함수를 실행할거에요 -> 제자리에서 중력만 받는 ApplyGravityOnly를 + return; // 중단할거에요 -> 이동을 막기 위해 함수를 } // 3. 이동 속도 계산 - // Shift 눌렀으면 달리기 속도, 아니면 걷기 속도 - float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; + float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; // 값을 결정할거에요 -> 달리기 중이면 런 스피드를, 아니면 이동 스피드를 speed에 // 공격 차징 중이라면 속도를 느리게 만듦 (긴장감 조성) - if (attackScript != null && attackScript.IsCharging) + if (attackScript != null && attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있고 차징 중이라면 { - // Lerp를 써서 차징 단계에 따라 부드럽게 감속 - float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); - speed *= speedReduction; + float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); // 값을 계산할거에요 -> 차징 진행도에 따라 속도 배율을 줄여서 speedReduction에 + speed *= speedReduction; // 값을 곱할거에요 -> 현재 속도(speed)에 감속 배율을 } // 4. 최종 이동 벡터 계산 - // 방향 * 속도 * 시간(프레임 보정) = 이번 프레임에 움직일 거리 - Vector3 motion = _moveDir * speed * Time.deltaTime; + Vector3 motion = _moveDir * speed * Time.deltaTime; // 벡터를 계산할거에요 -> 방향 * 속도 * 시간을 곱해서 이번 프레임 이동량을 motion에 // 중력 계산 (Y축) - ApplyGravity(); - motion.y = _verticalVelocity * Time.deltaTime; + ApplyGravity(); // 함수를 실행할거에요 -> 수직 속도를 계산하는 ApplyGravity를 + motion.y = _verticalVelocity * Time.deltaTime; // 값을 넣을거에요 -> 수직 이동량(속도 * 시간)을 motion의 y값에 // ⭐ 실제 이동 실행 (여기서 벽 충돌 처리가 자동 수행됨) - _controller.Move(motion); - - // 5. 안전장치 가동 (승천 버그 체크) - //CheckAbnormalHeight(); + _controller.Move(motion); // 이동시킬거에요 -> 캐릭터 컨트롤러를 motion 벡터만큼 // 6. 애니메이션 업데이트 (걷기/뛰기 모션) - UpdateAnimation(); + UpdateAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateAnimation을 } - private void ApplyGravity() + private void ApplyGravity() // 함수를 선언할거에요 -> 중력을 계산하는 ApplyGravity를 { - if (_controller.isGrounded) + if (_controller.isGrounded) // 조건이 맞으면 실행할거에요 -> 캐릭터가 땅에 닿아있다면 { - // 땅에 닿아있어도 -2f 정도로 계속 눌러줘야 경사면에서 붕 뜨지 않음 - _verticalVelocity = -2f; + _verticalVelocity = -2f; // 값을 설정할거에요 -> 바닥에 딱 붙어있도록 약한 하향 속도(-2)를 _verticalVelocity에 } - else + else // 조건이 틀리면 실행할거에요 -> 공중에 떠 있다면 { - // 공중에 있다면 중력 가속도 누적 (점점 빨라짐) - _verticalVelocity += gravity * Time.deltaTime; + _verticalVelocity += gravity * Time.deltaTime; // 값을 더할거에요 -> 중력 가속도를 시간에 맞춰 _verticalVelocity에 } } // 키보드 이동 없이 중력만 적용하는 함수 (피격/공격 중 사용) - private void ApplyGravityOnly() + private void ApplyGravityOnly() // 함수를 선언할거에요 -> 중력만 적용해서 움직이는 ApplyGravityOnly를 { - ApplyGravity(); - _controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0)); - // CheckAbnormalHeight(); // 넉백되다가 날아가지 않게 체크 + ApplyGravity(); // 함수를 실행할거에요 -> 수직 속도를 계산하는 ApplyGravity를 + _controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0)); // 이동시킬거에요 -> 수직 방향으로만 캐릭터를 } - - /// ⭐ 비정상 높이 감지 및 보정 - - //private void CheckAbnormalHeight() - //{ - // // 내 발밑으로 레이저를 쏴서 땅까지의 거리를 잽니다. - // RaycastHit hit; - // if (Physics.Raycast(transform.position, Vector3.down, out hit, 100f)) - // { - // float heightAboveGround = transform.position.y - hit.point.y; - - // // 땅에서 3미터 이상 떠있는데, 시스템상으론 점프 상태가 아니라면? (버그!) - // if (heightAboveGround > 3f && !_controller.isGrounded) - // { - // // 강제로 땅바닥 위치로 좌표를 계산 - // Vector3 correctedPos = transform.position; - // correctedPos.y = hit.point.y + _controller.height / 2f + minGroundHeight; - - // // 순간이동 시킬 땐 CharacterController를 잠시 꺼야 안전함 - // _controller.enabled = false; - // transform.position = correctedPos; - // _controller.enabled = true; - - // _verticalVelocity = 0f; // 낙하 속도 초기화 - - // Debug.LogWarning("[PlayerMovement] 비정상적인 높이 감지! 위치 보정함"); - // } - // } - //} - - private void UpdateAnimation() + private void UpdateAnimation() // 함수를 선언할거에요 -> 애니메이션 파라미터를 조절하는 UpdateAnimation을 { - if (pAnim == null) return; - // 이동 입력이 있으면 1(달리기) 또는 0.5(걷기), 없으면 0(대기) - float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; - // 차징 중이면 무조건 걷기 모션(0.5) 이하로 만듦 - if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; - pAnim.UpdateMove(animVal); + if (pAnim == null) return; // 조건이 맞으면 중단할거에요 -> 애니메이터 스크립트가 없다면 + + float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; // 값을 결정할거에요 -> 움직임이 있으면 (달리기면 1.0, 걷기면 0.5), 없으면 0을 animVal에 + + if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; // 조건이 맞으면 실행할거에요 -> 차징 중이라면 애니메이션 속도를 절반으로 줄여서 + + pAnim.UpdateMove(animVal); // 함수를 실행할거에요 -> 계산된 애니메이션 값(animVal)을 전달하는 UpdateMove를 } // 대시 관련 - private IEnumerator DashRoutine() + private IEnumerator DashRoutine() // 코루틴 함수를 선언할거에요 -> 대시 로직을 수행할 DashRoutine을 { - _isDashing = true; - _lastDashTime = Time.time; + _isDashing = true; // 상태를 바꿀거에요 -> 대시 중 상태를 참(true)으로 + _lastDashTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 대시 시간으로 // 이동 중이면 그 방향으로, 멈춰있으면 뒤로 회피(백스탭) - Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward; + Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward; // 벡터를 결정할거에요 -> 이동 중이면 이동 방향, 아니면 뒤쪽 방향을 dashDir에 // 무적 판정 켜기 (소울류 게임 회피 느낌) - if (health != null) health.isInvincible = true; + if (health != null) health.isInvincible = true; // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 무적 상태를 켜기(true)로 - float startTime = Time.time; - // 정해진 시간(0.08초) 동안 반복 - while (Time.time < startTime + dashDuration) + float startTime = Time.time; // 값을 저장할거에요 -> 대시 시작 시간을 startTime에 + + while (Time.time < startTime + dashDuration) // 반복할거에요 -> 현재 시간이 (시작시간 + 대시지속시간)보다 작을 동안 { - // 속도 = 거리 / 시간 - float speed = dashDistance / dashDuration; - Vector3 dashMotion = dashDir * speed * Time.deltaTime; + float speed = dashDistance / dashDuration; // 값을 계산할거에요 -> 거리 나누기 시간으로 대시 속도를 구해서 speed에 + Vector3 dashMotion = dashDir * speed * Time.deltaTime; // 벡터를 계산할거에요 -> 대시 방향 * 속도 * 시간을 곱해 이동량을 dashMotion에 // ⭐ transform.position += ... 대신 Move()를 쓰는 이유: // Move를 써야 대시 도중 벽을 만나면 뚫지 않고 멈줌 - CollisionFlags flags = _controller.Move(dashMotion); + CollisionFlags flags = _controller.Move(dashMotion); // 이동시킬거에요 -> 캐릭터 컨트롤러를 대시 이동량만큼 - yield return null; // 한 프레임 대기 + yield return null; // 대기할거에요 -> 다음 프레임까지 한 턴을 } // 무적 끄기 및 상태 해제 - if (health != null) health.isInvincible = false; - _isDashing = false; + if (health != null) health.isInvincible = false; // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 무적 상태를 끄기(false)로 + _isDashing = false; // 상태를 바꿀거에요 -> 대시 중 상태를 거짓(false)으로 } // 다른 스크립트에서 상태를 물어볼 때 쓰는 함수들 - public bool IsGrounded() => _controller.isGrounded; - public bool IsDashing() => _isDashing; - public float GetCurrentSpeed() => _controller.velocity.magnitude; + public bool IsGrounded() => _controller.isGrounded; // 값을 반환할거에요 -> 캐릭터가 땅에 닿아있는지 여부를 + public bool IsDashing() => _isDashing; // 값을 반환할거에요 -> 현재 대시 중인지 여부를 + public float GetCurrentSpeed() => _controller.velocity.magnitude; // 값을 반환할거에요 -> 현재 캐릭터의 실제 이동 속도 크기를 } \ No newline at end of file diff --git a/Assets/Scripts/Player/Effect/Player Effect.cs b/Assets/Scripts/Player/Effect/Player Effect.cs index d03d3079..3fa3b11b 100644 --- a/Assets/Scripts/Player/Effect/Player Effect.cs +++ b/Assets/Scripts/Player/Effect/Player Effect.cs @@ -1,41 +1,41 @@ -using UnityEngine; +using UnityEngine; // Ƽ ⺻ ҷðſ -> UnityEngine /// /// ÷̾ ð(VFX) û(SFX) ȿ ϴ ũƮ /// -public class PlayerEffects : MonoBehaviour +public class PlayerEffects : MonoBehaviour // Ŭ Ұſ -> MonoBehaviour ӹ޴ PlayerEffects { - [Header("--- ð ȿ (VFX) ---")] - [SerializeField] private GameObject[] slashEffects; // ޺ ٸ Ʈ - [SerializeField] private Transform slashSpawnPoint; + [Header("--- ð ȿ (VFX) ---")] // ν â ǥҰſ -> --- ð ȿ (VFX) --- + [SerializeField] private GameObject[] slashEffects; // 迭 Ұſ -> Ʈ յ + [SerializeField] private Transform slashSpawnPoint; // Ұſ -> Ʈ ġ - [Header("--- û ȿ (SFX) ---")] - [SerializeField] private AudioClip[] swingSounds; - private AudioSource _audioSource; + [Header("--- û ȿ (SFX) ---")] // ν â ǥҰſ -> --- û ȿ (SFX) --- + [SerializeField] private AudioClip[] swingSounds; // 迭 Ұſ -> ֵθ Ҹ Ŭ + private AudioSource _audioSource; // Ұſ -> ҽ Ʈ - private void Awake() + private void Awake() // Լ Ұſ -> ũƮ Awake { - _audioSource = GetComponent(); - if (_audioSource == null) _audioSource = gameObject.AddComponent(); + _audioSource = GetComponent(); // Ʈ ðſ -> ҽ + if (_audioSource == null) _audioSource = gameObject.AddComponent(); // ߰Ұſ -> ҽ Ʈ } /// /// ִϸ̼ ̺Ʈ ȣ Լ /// /// ޺ ȣ (0~2) - public void PlaySlashEffect(int comboIndex) + public void PlaySlashEffect(int comboIndex) // Լ Ұſ -> Ʈ ϴ PlaySlashEffect { // 1. - if (swingSounds.Length > comboIndex && swingSounds[comboIndex] != null) + if (swingSounds.Length > comboIndex && swingSounds[comboIndex] != null) // Ұſ -> ش ε Ҹ ִٸ { - _audioSource.PlayOneShot(swingSounds[comboIndex]); + _audioSource.PlayOneShot(swingSounds[comboIndex]); // Ұſ -> ش Ŭ } // 2. Ʈ - if (slashEffects.Length > comboIndex && slashEffects[comboIndex] != null && slashSpawnPoint != null) + if (slashEffects.Length > comboIndex && slashEffects[comboIndex] != null && slashSpawnPoint != null) // Ұſ -> ش ε Ʈ ġ ִٸ { - GameObject slash = Instantiate(slashEffects[comboIndex], slashSpawnPoint.position, slashSpawnPoint.rotation); - Destroy(slash, 1.0f); + GameObject slash = Instantiate(slashEffects[comboIndex], slashSpawnPoint.position, slashSpawnPoint.rotation); // Ұſ -> Ʈ Ʈ ġ + Destroy(slash, 1.0f); // ıҰſ -> Ʈ 1 ڿ } } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Equipment/EquipItem.cs b/Assets/Scripts/Player/Equipment/EquipItem.cs index bca24998..328a228c 100644 --- a/Assets/Scripts/Player/Equipment/EquipItem.cs +++ b/Assets/Scripts/Player/Equipment/EquipItem.cs @@ -1,93 +1,101 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class EquippableItem : MonoBehaviour -{ - [SerializeField] private WeaponConfig config; - public WeaponConfig Config => config; +public class EquippableItem : MonoBehaviour // 클래스를 선언할거에요 -> 장착/드랍/투척 가능한 아이템을 처리하는 EquippableItem을 +{ // 코드 블록을 시작할거에요 -> EquippableItem 범위를 - [Header("--- 데미지 밸런스 ---")] - [SerializeField] private float lv1Mult = 1.0f; - [SerializeField] private float lv2Mult = 1.5f; - [SerializeField] private float lv3Mult = 2.5f; - // [제거] strengthBonusFactor 변수 삭제 + [SerializeField] private WeaponConfig config; // 변수를 선언할거에요 -> 이 아이템이 참조할 무기 설정(WeaponConfig)을 config에 + public WeaponConfig Config => config; // 프로퍼티를 선언할거에요 -> 외부에서 config를 읽을 수 있게 Config로 노출 - private Rigidbody _rb; - private Collider _col; - private bool _isThrown; - private int _chargeLevel; - private Stats _thrower; - private Vector3 _originalWorldScale; + [Header("--- 데미지 밸런스 ---")] // 인스펙터에 제목을 표시할거에요 -> 데미지 밸런스 섹션을 + [SerializeField] private float lv1Mult = 1.0f; // 변수를 선언할거에요 -> 차지 1단계 배율을 lv1Mult에 + [SerializeField] private float lv2Mult = 1.5f; // 변수를 선언할거에요 -> 차지 2단계 배율을 lv2Mult에 + [SerializeField] private float lv3Mult = 2.5f; // 변수를 선언할거에요 -> 차지 3단계 배율을 lv3Mult에 + // [제거] strengthBonusFactor 변수 삭제 // 설명을 적을거에요 -> 힘 보너스 배율은 더 이상 사용하지 않음 - private void Awake() - { - _rb = GetComponent(); - _col = GetComponent(); - _originalWorldScale = transform.lossyScale; - } + private Rigidbody _rb; // 변수를 선언할거에요 -> 리지드바디를 저장할 _rb를 + private Collider _col; // 변수를 선언할거에요 -> 콜라이더를 저장할 _col을 + private bool _isThrown; // 변수를 선언할거에요 -> 현재 투척 상태인지 저장할 _isThrown을 + private int _chargeLevel; // 변수를 선언할거에요 -> 투척 당시 차지 레벨을 저장할 _chargeLevel을 + private Stats _thrower; // 변수를 선언할거에요 -> 투척한 주체(플레이어 스탯)를 저장할 _thrower를 + private Vector3 _originalWorldScale; // 변수를 선언할거에요 -> 원래 월드 스케일을 저장할 _originalWorldScale을 - public void OnPickedUp(Transform hand) - { - _isThrown = false; - transform.SetParent(hand); - transform.localScale = new Vector3( - _originalWorldScale.x / hand.lossyScale.x, - _originalWorldScale.y / hand.lossyScale.y, - _originalWorldScale.z / hand.lossyScale.z - ); - transform.localPosition = Vector3.zero; - transform.localRotation = Quaternion.identity; - if (_rb) _rb.isKinematic = true; - if (_col) _col.enabled = false; - } + private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + _rb = GetComponent(); // 컴포넌트를 가져올거에요 -> Rigidbody를 찾아 _rb에 저장 + _col = GetComponent(); // 컴포넌트를 가져올거에요 -> Collider를 찾아 _col에 저장 + _originalWorldScale = transform.lossyScale; // 값을 저장할거에요 -> 현재 월드 기준 크기를 원본으로 기억 + } // 코드 블록을 끝낼거에요 -> Awake를 - public void OnDropped(Vector3 throwDir) - { - _isThrown = false; - transform.SetParent(null); - transform.localScale = _originalWorldScale; - if (_rb) - { - _rb.isKinematic = false; - Vector3 force = (throwDir + Vector3.up * 0.5f).normalized * 5f; - _rb.AddForce(force, ForceMode.Impulse); - _rb.AddTorque(Random.insideUnitSphere * 3f, ForceMode.Impulse); - } - if (_col) _col.enabled = true; - } + public void OnPickedUp(Transform hand) // 함수를 선언할거에요 -> 아이템을 손에 집었을 때 처리하는 OnPickedUp을 + { // 코드 블록을 시작할거에요 -> OnPickedUp 범위를 + _isThrown = false; // 상태를 바꿀거에요 -> 투척 상태를 해제(false)로 + transform.SetParent(hand); // 부모를 바꿀거에요 -> 손(Hand) 트랜스폼에 붙이기 - public void OnThrown(Vector3 dir, float force, int lv, Stats s) - { - _isThrown = true; - _chargeLevel = lv; - _thrower = s; - transform.SetParent(null); - transform.localScale = _originalWorldScale; - if (_rb) - { - _rb.isKinematic = false; - _rb.AddForce(dir * force, ForceMode.Impulse); - _rb.AddTorque(transform.right * 10f, ForceMode.Impulse); - } - if (_col) _col.enabled = true; - } + transform.localScale = new Vector3( // 값을 설정할거에요 -> 손의 스케일 변화에도 월드 크기 유지되게 로컬 스케일 계산 + _originalWorldScale.x / hand.lossyScale.x, // x축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로 + _originalWorldScale.y / hand.lossyScale.y, // y축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로 + _originalWorldScale.z / hand.lossyScale.z // z축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로 + ); // 로컬 스케일 설정을 끝낼거에요 -> 월드 크기 유지 - private void OnCollisionEnter(Collision collision) - { - if (!_isThrown) return; - if (_thrower != null && collision.gameObject == _thrower.gameObject) return; + transform.localPosition = Vector3.zero; // 위치를 맞출거에요 -> 손 기준 로컬 위치 0으로 + transform.localRotation = Quaternion.identity; // 회전을 맞출거에요 -> 손 기준 로컬 회전 초기화 + if (_rb) _rb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 리지드바디가 있으면 물리 비활성(키네마틱)으로 + if (_col) _col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있으면 꺼서 손에서 충돌 안 나게 + } // 코드 블록을 끝낼거에요 -> OnPickedUp을 - if (collision.gameObject.TryGetComponent(out var target)) - { - float mult = _chargeLevel == 3 ? lv3Mult : (_chargeLevel == 2 ? lv2Mult : lv1Mult); + public void OnDropped(Vector3 throwDir) // 함수를 선언할거에요 -> 아이템을 떨어뜨릴 때 처리하는 OnDropped를 + { // 코드 블록을 시작할거에요 -> OnDropped 범위를 + _isThrown = false; // 상태를 바꿀거에요 -> 투척 상태가 아님(false)으로 + transform.SetParent(null); // 부모를 해제할거에요 -> 월드에 놓기 + transform.localScale = _originalWorldScale; // 스케일을 되돌릴거에요 -> 원래 월드 스케일로 - // ✨ [수정] 힘 보너스 로직 제거. (플레이어 기본 공격력 + 무기 대미지) * 차지 배율 - float finalDamage = (_thrower.BaseAttackDamage + config.BaseDamage) * mult; + if (_rb) // 조건을 검사할거에요 -> 리지드바디가 있는지 + { // 코드 블록을 시작할거에요 -> 물리 드랍 처리 + _rb.isKinematic = false; // 값을 바꿀거에요 -> 물리 활성화 + Vector3 force = (throwDir + Vector3.up * 0.5f).normalized * 5f; // 힘을 계산할거에요 -> 던지는 방향 + 살짝 위로 + 세기 5 + _rb.AddForce(force, ForceMode.Impulse); // 힘을 줄거에요 -> 순간 힘(Impulse)으로 튕기듯 드랍 + _rb.AddTorque(Random.insideUnitSphere * 3f, ForceMode.Impulse); // 회전도 줄거에요 -> 랜덤 토크로 자연스럽게 굴러가게 + } // 코드 블록을 끝낼거에요 -> 물리 드랍 처리 - target.TakeDamage(finalDamage); - Debug.Log($"[투척 적중] {collision.gameObject.name}에게 {finalDamage:F1} 데미지!"); - _isThrown = false; - } + if (_col) _col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더를 다시 켜기 + } // 코드 블록을 끝낼거에요 -> OnDropped를 - if (collision.gameObject.layer != LayerMask.NameToLayer("Player")) _isThrown = false; - } -} \ No newline at end of file + public void OnThrown(Vector3 dir, float force, int lv, Stats s) // 함수를 선언할거에요 -> 투척 시작 세팅을 하는 OnThrown을 + { // 코드 블록을 시작할거에요 -> OnThrown 범위를 + _isThrown = true; // 상태를 바꿀거에요 -> 투척 상태(true)로 + _chargeLevel = lv; // 값을 저장할거에요 -> 투척 당시 차지 레벨을 + _thrower = s; // 값을 저장할거에요 -> 던진 주체(스탯)를 저장 + transform.SetParent(null); // 부모를 해제할거에요 -> 월드로 분리 + transform.localScale = _originalWorldScale; // 스케일을 되돌릴거에요 -> 원래 월드 스케일로 + + if (_rb) // 조건을 검사할거에요 -> 리지드바디가 있는지 + { // 코드 블록을 시작할거에요 -> 물리 투척 처리 + _rb.isKinematic = false; // 값을 바꿀거에요 -> 물리 활성화 + _rb.AddForce(dir * force, ForceMode.Impulse); // 힘을 줄거에요 -> 던지는 방향 * force로 순간 가속 + _rb.AddTorque(transform.right * 10f, ForceMode.Impulse); // 회전을 줄거에요 -> 오른쪽 축으로 스핀 주기 + } // 코드 블록을 끝낼거에요 -> 물리 투척 처리 + + if (_col) _col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더를 켜서 충돌 가능하게 + } // 코드 블록을 끝낼거에요 -> OnThrown를 + + private void OnCollisionEnter(Collision collision) // 함수를 선언할거에요 -> 충돌이 일어나면 호출되는 OnCollisionEnter를 + { // 코드 블록을 시작할거에요 -> OnCollisionEnter 범위를 + if (!_isThrown) return; // 조건이 맞으면 종료할거에요 -> 투척 상태가 아니면 무시 + if (_thrower != null && collision.gameObject == _thrower.gameObject) return; // 조건이 맞으면 종료할거에요 -> 던진 본인에게는 맞지 않게 + + if (collision.gameObject.TryGetComponent(out var target)) // 조건을 검사할거에요 -> 맞은 대상이 데미지를 받을 수 있는지 + { // 코드 블록을 시작할거에요 -> 데미지 처리 + float mult = _chargeLevel == 3 ? lv3Mult : (_chargeLevel == 2 ? lv2Mult : lv1Mult); // 배율을 고를거에요 -> 차지 레벨에 따라 lv1/2/3 배율 + + // ✨ [수정] 힘 보너스 로직 제거. (플레이어 기본 공격력 + 무기 대미지) * 차지 배율 // 설명을 적을거에요 -> 최종 데미지 계산 공식 + float finalDamage = (_thrower.BaseAttackDamage + config.BaseDamage) * mult; // 값을 계산할거에요 -> (플레이어 공격력 + 무기 기본 데미지) * 배율 + + target.TakeDamage(finalDamage); // 함수를 실행할거에요 -> 대상에게 finalDamage만큼 데미지 주기 + Debug.Log($"[투척 적중] {collision.gameObject.name}에게 {finalDamage:F1} 데미지!"); // 로그를 찍을거에요 -> 적중 결과 출력 + _isThrown = false; // 상태를 바꿀거에요 -> 한 번 맞으면 투척 상태 해제 + } // 코드 블록을 끝낼거에요 -> 데미지 처리 + + if (collision.gameObject.layer != LayerMask.NameToLayer("Player")) _isThrown = false; // 조건이 맞으면 실행할거에요 -> 플레이어 레이어가 아니면 투척 상태 해제 + } // 코드 블록을 끝낼거에요 -> OnCollisionEnter를 + +} // 코드 블록을 끝낼거에요 -> EquippableItem을 \ No newline at end of file diff --git a/Assets/Scripts/Player/Interaction/Heal/HealthAltar.cs b/Assets/Scripts/Player/Interaction/Heal/HealthAltar.cs index cc25ac9d..de67b64b 100644 --- a/Assets/Scripts/Player/Interaction/Heal/HealthAltar.cs +++ b/Assets/Scripts/Player/Interaction/Heal/HealthAltar.cs @@ -1,54 +1,54 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class HealthAltar : MonoBehaviour +public class HealthAltar : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 HealthAltar를 { - [Header("--- 회복 설정 ---")] - [SerializeField] private float healAmount = 50f; // 상호작용 시 즉시 회복량 - [SerializeField] private float interactRange = 3.5f; // 상호작용 가능 거리 + [Header("--- 회복 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 회복 설정 --- 을 + [SerializeField] private float healAmount = 50f; // 변수를 선언할거에요 -> 회복량인 healAmount를 + [SerializeField] private float interactRange = 3.5f; // 변수를 선언할거에요 -> 상호작용 거리인 interactRange를 - [Header("--- 시각 효과 ---")] - [SerializeField] private ParticleSystem healEffect; + [Header("--- 시각 효과 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 시각 효과 --- 를 + [SerializeField] private ParticleSystem healEffect; // 변수를 선언할거에요 -> 회복 이펙트인 healEffect를 // ⭐ [핵심 수정] PlayerInteraction에서 던져주는 'PlayerHealth' 자료형을 직접 받습니다! // - public void Use(PlayerHealth playerHealth) + public void Use(PlayerHealth playerHealth) // 함수를 선언할거에요 -> 제단을 사용하는 Use를 { - if (playerHealth == null) return; + if (playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 체력 스크립트가 없다면 // 플레이어의 Transform 정보 추출 - Transform interactor = playerHealth.transform; + Transform interactor = playerHealth.transform; // 값을 가져올거에요 -> 상호작용하는 대상의 위치 정보를 // 규칙: 실제 몸통 중심점($Center$)과의 거리 계산 - float distance = Vector3.Distance(transform.position, interactor.position); + float distance = Vector3.Distance(transform.position, interactor.position); // 거리를 계산할거에요 -> 제단과 플레이어 사이의 거리를 - if (distance <= interactRange) + if (distance <= interactRange) // 조건이 맞으면 실행할거에요 -> 거리가 상호작용 범위 이내라면 { - ApplyHeal(playerHealth); // 컴포넌트 자체를 넘겨줌 + ApplyHeal(playerHealth); // 함수를 실행할거에요 -> 회복 적용 함수 ApplyHeal을 // 컴포넌트 자체를 넘겨줌 } - else + else // 조건이 틀리면 실행할거에요 -> 거리가 너무 멀다면 { - Debug.Log($"[제단] 너무 멉니다! (거리: {distance:F1} / 제한: {interactRange})"); + Debug.Log($"[제단] 너무 멉니다! (거리: {distance:F1} / 제한: {interactRange})"); // 로그를 출력할거에요 -> 거리 부족 메시지를 } } - private void ApplyHeal(PlayerHealth targetHealth) + private void ApplyHeal(PlayerHealth targetHealth) // 함수를 선언할거에요 -> 실제로 체력을 회복시키는 ApplyHeal을 { // ⭐ 캡슐화 규칙: 플레이어의 Health 스크립트에 있는 기능을 호출합니다. // 유저님의 프로젝트에 정의된 회복 함수 이름을 사용하세요 (예: RestoreHealth, AddHealth 등) - // targetHealth.RestoreHealth(healAmount); // + targetHealth.Heal(healAmount); // 함수를 실행할거에요 -> 플레이어의 회복 함수인 Heal을 // - if (healEffect != null) + if (healEffect != null) // 조건이 맞으면 실행할거에요 -> 이펙트가 설정되어 있다면 { - healEffect.transform.position = targetHealth.transform.position; - healEffect.Play(); + healEffect.transform.position = targetHealth.transform.position; // 위치를 옮길거에요 -> 플레이어 위치로 + healEffect.Play(); // 실행할거에요 -> 이펙트 재생을 } - Debug.Log($"[제단] {targetHealth.gameObject.name} 상호작용 성공! {healAmount} HP 회복."); + Debug.Log($"[제단] {targetHealth.gameObject.name} 상호작용 성공! {healAmount} HP 회복."); // 로그를 출력할거에요 -> 회복 성공 메시지를 } - private void OnDrawGizmosSelected() + private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 선택 시 기즈모를 그리는 OnDrawGizmosSelected를 { - Gizmos.color = Color.cyan; - Gizmos.DrawWireSphere(transform.position, interactRange); + Gizmos.color = Color.cyan; // 색상을 설정할거에요 -> 하늘색으로 + Gizmos.DrawWireSphere(transform.position, interactRange); // 그림을 그릴거에요 -> 상호작용 범위를 표시하는 원을 } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Interaction/PlayerInteraction.cs b/Assets/Scripts/Player/Interaction/PlayerInteraction.cs index 7895295c..afcdae16 100644 --- a/Assets/Scripts/Player/Interaction/PlayerInteraction.cs +++ b/Assets/Scripts/Player/Interaction/PlayerInteraction.cs @@ -1,44 +1,44 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class PlayerInteraction : MonoBehaviour +public class PlayerInteraction : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerInteraction을 { - [Header("--- 설정 ---")] - [SerializeField] private float interactRange = 3f; - [SerializeField] private LayerMask itemLayer; - [SerializeField] private Transform handSlot; - [SerializeField] private Stats playerStats; + [Header("--- 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 설정 --- 을 + [SerializeField] private float interactRange = 3f; // 변수를 선언할거에요 -> 상호작용 거리인 interactRange를 + [SerializeField] private LayerMask itemLayer; // 변수를 선언할거에요 -> 아이템 레이어인 itemLayer를 + [SerializeField] private Transform handSlot; // 변수를 선언할거에요 -> 무기 장착 위치인 handSlot을 + [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트인 playerStats를 - private EquippableItem _currentWeapon; - public EquippableItem CurrentWeapon => _currentWeapon; + private EquippableItem _currentWeapon; // 변수를 선언할거에요 -> 현재 장착된 무기인 _currentWeapon을 + public EquippableItem CurrentWeapon => _currentWeapon; // 프로퍼티를 선언할거에요 -> 현재 무기를 반환하는 CurrentWeapon을 - public void TryInteract() + public void TryInteract() // 함수를 선언할거에요 -> 상호작용을 시도하는 TryInteract를 { - Collider[] hits = Physics.OverlapSphere(transform.position, interactRange, itemLayer); - foreach (var hit in hits) + Collider[] hits = Physics.OverlapSphere(transform.position, interactRange, itemLayer); // 배열을 만들거에요 -> 주변 아이템들을 감지해서 hits에 + foreach (var hit in hits) // 반복할거에요 -> 감지된 모든 아이템에 대해 { // [MODIFIED] ArrowItem 제거 → ArrowPickup으로 통일 - if (hit.TryGetComponent(out var arrowPickup)) + if (hit.TryGetComponent(out var arrowPickup)) // 조건이 맞으면 실행할거에요 -> 대상이 화살 아이템이라면 { - PickupArrow(arrowPickup); - break; + PickupArrow(arrowPickup); // 함수를 실행할거에요 -> 화살 줍기 함수 PickupArrow를 + break; // 중단할거에요 -> 반복문을 (하나만 줍기 위해) } - if (hit.TryGetComponent(out var item)) + if (hit.TryGetComponent(out var item)) // 조건이 맞으면 실행할거에요 -> 대상이 장착 가능한 무기라면 { - EquipWeapon(item); - break; + EquipWeapon(item); // 함수를 실행할거에요 -> 무기 장착 함수 EquipWeapon을 + break; // 중단할거에요 -> 반복문을 } - if (hit.TryGetComponent(out var potion)) + if (hit.TryGetComponent(out var potion)) // 조건이 맞으면 실행할거에요 -> 대상이 회복 물약이라면 { - potion.Use(GetComponent()); - break; + potion.Use(GetComponent()); // 함수를 실행할거에요 -> 물약 사용 함수 Use를 + break; // 중단할거에요 -> 반복문을 } - if (hit.TryGetComponent(out var altar)) + if (hit.TryGetComponent(out var altar)) // 조건이 맞으면 실행할거에요 -> 대상이 회복 제단이라면 { - altar.Use(GetComponent()); - break; + altar.Use(GetComponent()); // 함수를 실행할거에요 -> 제단 사용 함수 Use를 + break; // 중단할거에요 -> 반복문을 } } } @@ -47,47 +47,47 @@ public class PlayerInteraction : MonoBehaviour /// [MODIFIED] ArrowPickup을 직접 처리 /// 기존: ArrowItem 타입 → 변경: ArrowPickup 타입으로 통일 /// - private void PickupArrow(ArrowPickup arrowPickup) + private void PickupArrow(ArrowPickup arrowPickup) // 함수를 선언할거에요 -> 화살을 줍는 PickupArrow를 { - if (arrowPickup == null) return; + if (arrowPickup == null) return; // 조건이 맞으면 중단할거에요 -> 화살 아이템이 없다면 - PlayerAttack playerAttack = GetComponent(); - if (playerAttack != null) + PlayerAttack playerAttack = GetComponent(); // 컴포넌트를 찾을거에요 -> 플레이어 공격 스크립트를 + if (playerAttack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면 { // ArrowPickup.Pickup() 내부에서 // playerAttack.SetCurrentArrow(ArrowData)를 호출합니다 - arrowPickup.Pickup(playerAttack); + arrowPickup.Pickup(playerAttack); // 실행할거에요 -> 화살 아이템의 줍기 함수를 } } - private void EquipWeapon(EquippableItem item) + private void EquipWeapon(EquippableItem item) // 함수를 선언할거에요 -> 무기를 장착하는 EquipWeapon을 { - if (_currentWeapon != null) _currentWeapon.OnDropped(transform.forward); - _currentWeapon = item; - _currentWeapon.OnPickedUp(handSlot); + if (_currentWeapon != null) _currentWeapon.OnDropped(transform.forward); // 조건이 맞으면 실행할거에요 -> 이미 든 무기가 있다면 떨구기를 + _currentWeapon = item; // 값을 저장할거에요 -> 새 무기를 현재 무기로 + _currentWeapon.OnPickedUp(handSlot); // 실행할거에요 -> 무기의 줍기 함수 OnPickedUp을 - if (playerStats != null) + if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 플레이어 스탯이 있다면 { - playerStats.weaponDamage = item.Config.BaseDamage; + playerStats.weaponDamage = item.Config.BaseDamage; // 값을 설정할거에요 -> 무기 공격력을 스탯에 반영하기를 } - FindObjectOfType()?.UpdateStatTexts(); + FindObjectOfType()?.UpdateStatTexts(); // 실행할거에요 -> 스탯 UI 갱신을 } - public void ClearCurrentWeapon() + public void ClearCurrentWeapon() // 함수를 선언할거에요 -> 현재 무기를 해제하는 ClearCurrentWeapon을 { - _currentWeapon = null; - if (playerStats != null) + _currentWeapon = null; // 값을 비울거에요 -> 현재 무기 변수를 + if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 스탯 스크립트가 있다면 { - playerStats.weaponDamage = 0; - FindObjectOfType()?.UpdateStatTexts(); + playerStats.weaponDamage = 0; // 값을 초기화할거에요 -> 무기 공격력을 0으로 + FindObjectOfType()?.UpdateStatTexts(); // 실행할거에요 -> 스탯 UI 갱신을 } // [NEW] 무기 해제 시 화살도 초기화 - PlayerAttack playerAttack = GetComponent(); - if (playerAttack != null) + PlayerAttack playerAttack = GetComponent(); // 컴포넌트를 찾을거에요 -> 공격 스크립트를 + if (playerAttack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면 { - playerAttack.ResetArrow(); + playerAttack.ResetArrow(); // 실행할거에요 -> 화살 초기화 함수 ResetArrow를 } } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Stats/PlayerLevelSystem.cs b/Assets/Scripts/Player/Stats/PlayerLevelSystem.cs index b2575d29..634fa299 100644 --- a/Assets/Scripts/Player/Stats/PlayerLevelSystem.cs +++ b/Assets/Scripts/Player/Stats/PlayerLevelSystem.cs @@ -1,70 +1,70 @@ -using System.Collections; -using System.Collections.Generic; -using TMPro; -using UnityEngine; -using UnityEngine.UI; +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 +using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를 -public class PlayerLevelSystem : MonoBehaviour +public class PlayerLevelSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerLevelSystem을 { - [Header("--- 참조 ---")] - [SerializeField] private Stats stats; - [SerializeField] private PlayerHealth pHealth; + [Header("--- 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 참조 --- 를 + [SerializeField] private Stats stats; // 변수를 선언할거에요 -> 스탯 스크립트인 stats를 + [SerializeField] private PlayerHealth pHealth; // 변수를 선언할거에요 -> 체력 스크립트인 pHealth를 - [Header("레벨 설정")] - public int level = 1; - public int currentExp = 0; - [SerializeField] private int[] expTable; + [Header("레벨 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 레벨 설정 을 + public int level = 1; // 변수를 초기화할거에요 -> 현재 레벨을 1로 + public int currentExp = 0; // 변수를 초기화할거에요 -> 현재 경험치를 0으로 + [SerializeField] private int[] expTable; // 배열을 선언할거에요 -> 레벨별 필요 경험치 테이블인 expTable을 - [Header("UI")] - [SerializeField] private Image expFillImage; - [SerializeField] private TextMeshProUGUI expText; - [SerializeField] private TextMeshProUGUI levelText; + [Header("UI")] // 인스펙터 창에 제목을 표시할거에요 -> UI 를 + [SerializeField] private Image expFillImage; // 변수를 선언할거에요 -> 경험치바 이미지인 expFillImage를 + [SerializeField] private TextMeshProUGUI expText; // 변수를 선언할거에요 -> 경험치 텍스트인 expText를 + [SerializeField] private TextMeshProUGUI levelText; // 변수를 선언할거에요 -> 레벨 텍스트인 levelText를 - public static System.Action OnLevelUp; + public static System.Action OnLevelUp; // 이벤트를 선언할거에요 -> 레벨업 시 호출될 정적 이벤트 OnLevelUp을 - private int RequiredExp + private int RequiredExp // 프로퍼티를 선언할거에요 -> 필요 경험치를 반환하는 RequiredExp를 { get { - int index = level - 1; - if (index >= expTable.Length) return expTable[expTable.Length - 1]; - return expTable[index]; + int index = level - 1; // 인덱스를 계산할거에요 -> 레벨에서 1을 뺀 값으로 + if (index >= expTable.Length) return expTable[expTable.Length - 1]; // 조건이 맞으면 반환할거에요 -> 만렙이면 마지막 필요 경험치를 + return expTable[index]; // 반환할거에요 -> 현재 레벨의 필요 경험치를 } } - private void OnEnable() { MonsterClass.OnMonsterKilled += GainExp; UpdateExpUI(); } - private void OnDisable() { MonsterClass.OnMonsterKilled -= GainExp; } + private void OnEnable() { MonsterClass.OnMonsterKilled += GainExp; UpdateExpUI(); } // 함수를 실행할거에요 -> 몬스터 처치 이벤트 구독과 UI 갱신을 + private void OnDisable() { MonsterClass.OnMonsterKilled -= GainExp; } // 함수를 실행할거에요 -> 몬스터 처치 이벤트 구독 해제를 - void GainExp(int amount) + void GainExp(int amount) // 함수를 선언할거에요 -> 경험치를 획득하는 GainExp를 { - currentExp += amount; - while (currentExp >= RequiredExp) { currentExp -= RequiredExp; LevelUp(); } - UpdateExpUI(); + currentExp += amount; // 값을 더할거에요 -> 획득한 경험치를 현재 경험치에 + while (currentExp >= RequiredExp) { currentExp -= RequiredExp; LevelUp(); } // 반복할거에요 -> 경험치가 꽉 찼다면 레벨업을 + UpdateExpUI(); // 실행할거에요 -> UI 갱신 함수를 } - void LevelUp() + void LevelUp() // 함수를 선언할거에요 -> 레벨업 처리를 하는 LevelUp을 { - if (level >= expTable.Length + 1) { currentExp = 0; return; } - level++; + if (level >= expTable.Length + 1) { currentExp = 0; return; } // 조건이 맞으면 중단할거에요 -> 최대 레벨이라면 경험치만 비우고 리턴 + level++; // 값을 증가시킬거에요 -> 레벨을 1만큼 // ✨ 힘 대신 공격력(+10) 증가 - if (stats != null) stats.AddBaseLevelUpStats(1000f, 10f); + if (stats != null) stats.AddBaseLevelUpStats(1000f, 10f); // 실행할거에요 -> 스탯 증가 함수를 (체력 1000, 공격력 10) - if (pHealth != null) pHealth.RefreshHealthUI(); - StartCoroutine(DelayedCardPopup()); + if (pHealth != null) pHealth.RefreshHealthUI(); // 실행할거에요 -> 체력 UI 갱신을 + StartCoroutine(DelayedCardPopup()); // 코루틴을 시작할거에요 -> 카드 선택 팝업 지연 실행을 } - private IEnumerator DelayedCardPopup() + private IEnumerator DelayedCardPopup() // 코루틴 함수를 선언할거에요 -> 카드 팝업을 지연시키는 DelayedCardPopup을 { - yield return new WaitForSeconds(1.5f); - OnLevelUp?.Invoke(); + yield return new WaitForSeconds(1.5f); // 기다릴거에요 -> 1.5초 동안 + OnLevelUp?.Invoke(); // 이벤트를 실행할거에요 -> 레벨업 알림 이벤트를 } - void UpdateExpUI() + void UpdateExpUI() // 함수를 선언할거에요 -> 경험치 UI를 갱신하는 UpdateExpUI를 { - float fill = (float)currentExp / RequiredExp; - if (expFillImage != null) expFillImage.fillAmount = fill; - if (expText != null) expText.text = $"{currentExp} / {RequiredExp}"; - if (levelText != null) levelText.text = $"Lv. {level}"; + float fill = (float)currentExp / RequiredExp; // 비율을 계산할거에요 -> 현재 경험치 나누기 필요 경험치로 + if (expFillImage != null) expFillImage.fillAmount = fill; // 값을 설정할거에요 -> 게이지 채움 정도를 + if (expText != null) expText.text = $"{currentExp} / {RequiredExp}"; // 텍스트를 바꿀거에요 -> 현재/필요 경험치 수치로 + if (levelText != null) levelText.text = $"Lv. {level}"; // 텍스트를 바꿀거에요 -> 현재 레벨로 } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Stats/StatType.cs b/Assets/Scripts/Player/Stats/StatType.cs index e22f8776..f1a3e12d 100644 --- a/Assets/Scripts/Player/Stats/StatType.cs +++ b/Assets/Scripts/Player/Stats/StatType.cs @@ -1,8 +1,7 @@ - -public enum StatType -{ - Health, - Speed, - // Strength, - Damage -} +public enum StatType // 열거형을 선언할거에요 -> 스탯 종류를 구분하는 StatType을 +{ // 코드 블록을 시작할거에요 -> StatType 범위를 + Health, // 값을 정의할거에요 -> 체력 스탯을 + Speed, // 값을 정의할거에요 -> 이동 속도 스탯을 + // Strength, // 주석 처리할거에요 -> 힘 스탯(현재 미사용) + Damage // 값을 정의할거에요 -> 공격 데미지 스탯을 +} // 코드 블록을 끝낼거에요 -> StatType을 \ No newline at end of file diff --git a/Assets/Scripts/Player/Stats/Stats.cs b/Assets/Scripts/Player/Stats/Stats.cs index c68fee1f..5eab9201 100644 --- a/Assets/Scripts/Player/Stats/Stats.cs +++ b/Assets/Scripts/Player/Stats/Stats.cs @@ -1,56 +1,56 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class Stats : MonoBehaviour +public class Stats : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 Stats를 { - [Header("--- 기본 능력치 ---")] - [SerializeField] private float baseMaxHealth = 100f; - [SerializeField] private float baseMoveSpeed = 5f; - [SerializeField] private float baseAttackDamage = 10f; + [Header("--- 기본 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 기본 능력치 --- 를 + [SerializeField] private float baseMaxHealth = 100f; // 변수를 선언할거에요 -> 기본 최대 체력인 baseMaxHealth를 + [SerializeField] private float baseMoveSpeed = 5f; // 변수를 선언할거에요 -> 기본 이동 속도인 baseMoveSpeed를 + [SerializeField] private float baseAttackDamage = 10f; // 변수를 선언할거에요 -> 기본 공격력인 baseAttackDamage를 - [Header("--- 보너스 능력치 ---")] - public float bonusMaxHealth; - public float bonusMoveSpeed; - public float bonusAttackDamage; + [Header("--- 보너스 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 보너스 능력치 --- 를 + public float bonusMaxHealth; // 변수를 선언할거에요 -> 추가 체력인 bonusMaxHealth를 + public float bonusMoveSpeed; // 변수를 선언할거에요 -> 추가 이동 속도인 bonusMoveSpeed를 + public float bonusAttackDamage; // 변수를 선언할거에요 -> 추가 공격력인 bonusAttackDamage를 - [Header("--- 장착 장비 ---")] - public float weaponDamage; + [Header("--- 장착 장비 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 장착 장비 --- 를 + public float weaponDamage; // 변수를 선언할거에요 -> 무기 데미지인 weaponDamage를 - [Header("--- 밸런스 설정 ---")] - [SerializeField] private float runSpeedMultiplier = 1.5f; + [Header("--- 밸런스 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 밸런스 설정 --- 을 + [SerializeField] private float runSpeedMultiplier = 1.5f; // 변수를 선언할거에요 -> 달리기 속도 배율인 runSpeedMultiplier를 /* ========================= * 실제 게임 로직용 프로퍼티 * ========================= */ - public float MaxHealth => baseMaxHealth + bonusMaxHealth; - public float BaseAttackDamage => baseAttackDamage + bonusAttackDamage; - public float TotalAttackDamage => BaseAttackDamage + weaponDamage; + public float MaxHealth => baseMaxHealth + bonusMaxHealth; // 프로퍼티를 선언할거에요 -> 총 최대 체력을 반환하는 MaxHealth를 + public float BaseAttackDamage => baseAttackDamage + bonusAttackDamage; // 프로퍼티를 선언할거에요 -> 총 기본 공격력을 반환하는 BaseAttackDamage를 + public float TotalAttackDamage => BaseAttackDamage + weaponDamage; // 프로퍼티를 선언할거에요 -> 최종 공격력(무기 포함)을 반환하는 TotalAttackDamage를 // ✨ [수정] 이제 무게 페널티 없이 순수 속도만 계산합니다. - public float CurrentMoveSpeed => baseMoveSpeed + bonusMoveSpeed; - public float CurrentRunSpeed => CurrentMoveSpeed * runSpeedMultiplier; + public float CurrentMoveSpeed => baseMoveSpeed + bonusMoveSpeed; // 프로퍼티를 선언할거에요 -> 현재 이동 속도를 반환하는 CurrentMoveSpeed를 + public float CurrentRunSpeed => CurrentMoveSpeed * runSpeedMultiplier; // 프로퍼티를 선언할거에요 -> 현재 달리기 속도를 반환하는 CurrentRunSpeed를 - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를 { - finalMaxHealth = MaxHealth; - finalMoveSpeed = CurrentMoveSpeed; - finalAttackDamage = TotalAttackDamage; + finalMaxHealth = MaxHealth; // 값을 갱신할거에요 -> 디버그용 최종 체력을 + finalMoveSpeed = CurrentMoveSpeed; // 값을 갱신할거에요 -> 디버그용 최종 속도를 + finalAttackDamage = TotalAttackDamage; // 값을 갱신할거에요 -> 디버그용 최종 공격력을 } // ✨ [수정] 레벨업 시 체력과 공격력을 올려주도록 변경 - public void AddBaseLevelUpStats(float hpAdd, float dmgAdd) + public void AddBaseLevelUpStats(float hpAdd, float dmgAdd) // 함수를 선언할거에요 -> 레벨업 보너스를 적용하는 AddBaseLevelUpStats를 { - baseMaxHealth += hpAdd; - baseAttackDamage += dmgAdd; + baseMaxHealth += hpAdd; // 값을 더할거에요 -> 기본 체력에 증가분을 + baseAttackDamage += dmgAdd; // 값을 더할거에요 -> 기본 공격력에 증가분을 } - public void AddMaxHealth(float value) => bonusMaxHealth += value; - public void AddMoveSpeed(float value) => bonusMoveSpeed += value; - public void AddAttackDamage(float value) => bonusAttackDamage += value; + public void AddMaxHealth(float value) => bonusMaxHealth += value; // 함수를 선언할거에요 -> 추가 체력을 늘리는 AddMaxHealth를 + public void AddMoveSpeed(float value) => bonusMoveSpeed += value; // 함수를 선언할거에요 -> 추가 속도를 늘리는 AddMoveSpeed를 + public void AddAttackDamage(float value) => bonusAttackDamage += value; // 함수를 선언할거에요 -> 추가 공격력을 늘리는 AddAttackDamage를 // ✨ [제거] 무게 및 힘 관련 함수들(UpdateWeaponWeight, ResetWeight)이 삭제되었습니다. - [Header("--- 최종 능력치 (Read Only) ---")] - [SerializeField] private float finalMaxHealth; - [SerializeField] private float finalMoveSpeed; - [SerializeField] private float finalAttackDamage; + [Header("--- 최종 능력치 (Read Only) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최종 능력치 (Read Only) --- 를 + [SerializeField] private float finalMaxHealth; // 변수를 선언할거에요 -> 디버그용 최종 체력 변수를 + [SerializeField] private float finalMoveSpeed; // 변수를 선언할거에요 -> 디버그용 최종 속도 변수를 + [SerializeField] private float finalAttackDamage; // 변수를 선언할거에요 -> 디버그용 최종 공격력 변수를 } \ No newline at end of file diff --git a/Assets/Scripts/Player/Upgrade/Data/CardData.cs b/Assets/Scripts/Player/Upgrade/Data/CardData.cs index a3745453..33faace5 100644 --- a/Assets/Scripts/Player/Upgrade/Data/CardData.cs +++ b/Assets/Scripts/Player/Upgrade/Data/CardData.cs @@ -1,27 +1,27 @@ -using System.Collections; -using System.Collections.Generic; -using TMPro; -using Unity.VisualScripting; -using UnityEngine; +using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을 +using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를 +using Unity.VisualScripting; // 유니티 비주얼 스크립팅 기능을 사용할거에요 -> Unity.VisualScripting을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -[CreateAssetMenu(menuName = "LevelUp/Card")] -public abstract class CardData : ScriptableObject +[CreateAssetMenu(menuName = "LevelUp/Card")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "LevelUp/Card"를 +public abstract class CardData : ScriptableObject // 추상 클래스를 선언할거에요 -> ScriptableObject를 상속받는 CardData를 { - public Sprite icon; + public Sprite icon; // 변수를 선언할거에요 -> 카드 아이콘 이미지를 icon에 // ⭐ 추가: 이 카드가 나타나기 위해 필요한 '복수의 집착' 레벨 // 기본값을 1로 설정하면 처음부터 등장합니다. - public int requiredObsessionLevel = 1; + public int requiredObsessionLevel = 1; // 변수를 선언할거에요 -> 등장 조건 레벨(1)을 requiredObsessionLevel에 // UI 표시용 - public abstract string GetText(); + public abstract string GetText(); // 추상 함수를 선언할거에요 -> 카드 설명 텍스트를 반환하는 GetText를 // 외부에서 호출되는 공통 실행 함수 - public void Execute() + public void Execute() // 함수를 선언할거에요 -> 카드 효과를 실행하는 공통 함수 Execute를 { - ApplyEffect(); + ApplyEffect(); // 함수를 실행할거에요 -> 실제 효과 적용 함수를 } // 실제 효과는 자식이 구현 - protected abstract void ApplyEffect(); -} + protected abstract void ApplyEffect(); // 추상 함수를 선언할거에요 -> 자식 클래스에서 구현할 효과 적용 함수 ApplyEffect를 +} \ No newline at end of file diff --git a/Assets/Scripts/Player/Upgrade/Data/RandomStatCardData.cs b/Assets/Scripts/Player/Upgrade/Data/RandomStatCardData.cs index dae62d45..055e852d 100644 --- a/Assets/Scripts/Player/Upgrade/Data/RandomStatCardData.cs +++ b/Assets/Scripts/Player/Upgrade/Data/RandomStatCardData.cs @@ -1,31 +1,31 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -[CreateAssetMenu(menuName = "LevelUp/RandomStatCard")] -public class RandomStatCardData : CardData +[CreateAssetMenu(menuName = "LevelUp/RandomStatCard")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "LevelUp/RandomStatCard"를 +public class RandomStatCardData : CardData // 클래스를 선언할거에요 -> CardData를 상속받는 RandomStatCardData를 { - public StatType[] possibleStats; - public int minValue = 1; - public int maxValue = 3; + public StatType[] possibleStats; // 배열을 선언할거에요 -> 등장 가능한 스탯 목록을 possibleStats에 + public int minValue = 1; // 변수를 선언할거에요 -> 스탯 증가 최소값을 minValue에 + public int maxValue = 3; // 변수를 선언할거에요 -> 스탯 증가 최대값을 maxValue에 // ⭐ 기존의 stat1, value1 같은 변수들은 다 지워버리세요! // 이제 CardUI가 기억할 거니까 여기엔 필요 없습니다. - public override string GetText() => ""; // CardUI에서 직접 만드니까 비워둡니다. + public override string GetText() => ""; // 함수를 덮어씌울거에요 -> 텍스트 반환 함수를 (CardUI가 처리하므로 빈 문자열 반환) - protected override void ApplyEffect() { } // 사용하지 않음 + protected override void ApplyEffect() { } // 함수를 덮어씌울거에요 -> 기본 효과 적용 함수를 (여기선 안 씀) // ⭐ CardUI가 호출할 수 있게 public으로 만듭니다. - public void ApplyToPlayer(StatType stat, int value) + public void ApplyToPlayer(StatType stat, int value) // 함수를 선언할거에요 -> 플레이어에게 스탯을 적용하는 ApplyToPlayer를 { - Stats stats = FindObjectOfType(); - if (stats == null) return; + Stats stats = FindObjectOfType(); // 컴포넌트를 찾을거에요 -> 씬에 있는 Stats 스크립트를 + if (stats == null) return; // 조건이 맞으면 중단할거에요 -> Stats가 없다면 - switch (stat) + switch (stat) // 분기할거에요 -> 스탯 타입(stat)에 따라 { - case StatType.Health: stats.AddMaxHealth(value); break; - case StatType.Speed: stats.AddMoveSpeed(value); break; - // case StatType.Strength: stats.AddStrength(value); break; - case StatType.Damage: stats.AddAttackDamage(value); break; + case StatType.Health: stats.AddMaxHealth(value); break; // 일치하면 실행할거에요 -> 최대 체력 증가 함수를 + case StatType.Speed: stats.AddMoveSpeed(value); break; // 일치하면 실행할거에요 -> 이동 속도 증가 함수를 + // case StatType.Strength: stats.AddStrength(value); break; // (주석 처리됨) 힘 증가 로직 + case StatType.Damage: stats.AddAttackDamage(value); break; // 일치하면 실행할거에요 -> 공격력 증가 함수를 } } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Upgrade/UI/CardUI.cs b/Assets/Scripts/Player/Upgrade/UI/CardUI.cs index 0f7ac132..a9f3164d 100644 --- a/Assets/Scripts/Player/Upgrade/UI/CardUI.cs +++ b/Assets/Scripts/Player/Upgrade/UI/CardUI.cs @@ -1,59 +1,59 @@ -using UnityEngine; -using TMPro; -using UnityEngine.UI; // +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를 +using UnityEngine.UI; // 유니티 UI 기능을 사용할거에요 -> UnityEngine.UI를 -public class CardUI : MonoBehaviour +public class CardUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 CardUI를 { - [SerializeField] private TextMeshProUGUI effectText; - [SerializeField] private Image iconImage; - [SerializeField] private Outline selectionOutline; // ⭐ 인스펙터에서 테두리 컴포넌트 연결 + [SerializeField] private TextMeshProUGUI effectText; // 변수를 선언할거에요 -> 효과 설명 텍스트 UI를 effectText에 + [SerializeField] private Image iconImage; // 변수를 선언할거에요 -> 아이콘 이미지 UI를 iconImage에 + [SerializeField] private Outline selectionOutline; // 변수를 선언할거에요 -> 선택 시 켜질 테두리 컴포넌트를 selectionOutline에 - private CardData cardData; - private LevelUpUIManager uiManager; + private CardData cardData; // 변수를 선언할거에요 -> 이 UI가 표시할 카드 데이터를 cardData에 + private LevelUpUIManager uiManager; // 변수를 선언할거에요 -> UI 매니저를 uiManager에 - private StatType s1, s2; - private int v1, v2; - private bool isRandomCard = false; + private StatType s1, s2; // 변수를 선언할거에요 -> 랜덤 스탯 타입 2개를 저장할 s1, s2를 + private int v1, v2; // 변수를 선언할거에요 -> 랜덤 스탯 수치 2개를 저장할 v1, v2를 + private bool isRandomCard = false; // 변수를 초기화할거에요 -> 랜덤 카드 여부를 거짓으로 - public void Setup(CardData data, LevelUpUIManager manager) + public void Setup(CardData data, LevelUpUIManager manager) // 함수를 선언할거에요 -> 카드를 설정하는 Setup을 { - cardData = data; - uiManager = manager; - if (selectionOutline != null) selectionOutline.enabled = false; // 처음엔 테두리 끔 + cardData = data; // 값을 저장할거에요 -> 전달받은 카드 데이터를 + uiManager = manager; // 값을 저장할거에요 -> 전달받은 매니저를 + if (selectionOutline != null) selectionOutline.enabled = false; // 조건이 맞으면 실행할거에요 -> 테두리가 있다면 끄기를 // (랜덤 스탯 카드 데이터 처리 로직 - 기존 코드 유지) - RandomStatCardData randomData = cardData as RandomStatCardData; - if (randomData != null) + RandomStatCardData randomData = cardData as RandomStatCardData; // 형변환을 시도할거에요 -> 카드 데이터를 랜덤 스탯 카드 데이터로 + if (randomData != null) // 조건이 맞으면 실행할거에요 -> 랜덤 스탯 카드가 맞다면 { - isRandomCard = true; - s1 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; - do { s2 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; } while (s1 == s2); - v1 = Random.Range(Mathf.Max(1, randomData.minValue), Mathf.Max(1, randomData.maxValue) + 1); - v2 = Random.Range(Mathf.Min(-1, randomData.minValue), -1 + 1); - effectText.text = $"{s1} +{v1}\n{s2} {v2}"; + isRandomCard = true; // 상태를 바꿀거에요 -> 랜덤 카드 상태를 참으로 + s1 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; // 값을 뽑을거에요 -> 첫 번째 스탯을 랜덤으로 + do { s2 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; } while (s1 == s2); // 반복할거에요 -> 두 번째 스탯이 첫 번째와 다를 때까지 뽑기를 + v1 = Random.Range(Mathf.Max(1, randomData.minValue), Mathf.Max(1, randomData.maxValue) + 1); // 값을 뽑을거에요 -> 첫 번째 수치를 범위 내 랜덤으로 + v2 = Random.Range(Mathf.Min(-1, randomData.minValue), -1 + 1); // 값을 뽑을거에요 -> 두 번째 수치를 (음수 범위 등 고려하여) 랜덤으로 + effectText.text = $"{s1} +{v1}\n{s2} {v2}"; // 텍스트를 설정할거에요 -> 뽑힌 스탯과 수치를 UI에 표시하도록 } - else { effectText.text = cardData.GetText(); } + else { effectText.text = cardData.GetText(); } // 조건이 틀리면 실행할거에요 -> 일반 카드라면 기본 텍스트를 표시하도록 } // 카드 클릭 시 매니저에게 알림 - public void OnClick() { uiManager.OnCardClick(this); } + public void OnClick() { uiManager.OnCardClick(this); } // 함수를 실행할거에요 -> 매니저에게 이 카드가 클릭되었음을 알리기를 // 선택 시 하이라이트 연출 - public void SetSelected(bool isSelected) + public void SetSelected(bool isSelected) // 함수를 선언할거에요 -> 선택 상태를 설정하는 SetSelected를 { - if (selectionOutline != null) selectionOutline.enabled = isSelected; - transform.localScale = isSelected ? Vector3.one * 1.05f : Vector3.one; + if (selectionOutline != null) selectionOutline.enabled = isSelected; // 조건이 맞으면 실행할거에요 -> 테두리를 켜거나 끄기를 + transform.localScale = isSelected ? Vector3.one * 1.05f : Vector3.one; // 크기를 조절할거에요 -> 선택되면 살짝 키우고 아니면 원래대로 } // APPLY 버튼 누를 때 최종 실행될 효과 - public void ApplyCurrentEffect() + public void ApplyCurrentEffect() // 함수를 선언할거에요 -> 효과를 최종 적용하는 ApplyCurrentEffect를 { - if (isRandomCard) + if (isRandomCard) // 조건이 맞으면 실행할거에요 -> 랜덤 카드라면 { - RandomStatCardData rd = cardData as RandomStatCardData; - rd.ApplyToPlayer(s1, v1); rd.ApplyToPlayer(s2, v2); + RandomStatCardData rd = cardData as RandomStatCardData; // 형변환할거에요 -> 데이터를 랜덤 카드 데이터로 + rd.ApplyToPlayer(s1, v1); rd.ApplyToPlayer(s2, v2); // 실행할거에요 -> 저장해둔 스탯(s1, s2)과 수치(v1, v2)를 플레이어에게 적용하기를 } - else { cardData.Execute(); } - FindObjectOfType()?.RefreshHealthUI(); + else { cardData.Execute(); } // 조건이 틀리면 실행할거에요 -> 일반 카드라면 기본 실행 함수를 + FindObjectOfType()?.RefreshHealthUI(); // 실행할거에요 -> 체력 UI 갱신을 (혹시 체력이 변했을 수 있으니) } } \ No newline at end of file diff --git a/Assets/Scripts/System_Scripts/SettingPanelController.cs b/Assets/Scripts/System_Scripts/SettingPanelController.cs index ef87eb20..da19db64 100644 --- a/Assets/Scripts/System_Scripts/SettingPanelController.cs +++ b/Assets/Scripts/System_Scripts/SettingPanelController.cs @@ -1,79 +1,79 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; +using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class SettingPanelController : MonoBehaviour +public class SettingPanelController : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 SettingPanelController를 { - [SerializeField] private GameObject settingPanel; - [SerializeField] private GameObject gameStopPanel; - [HideInInspector] public bool IsSettingOpen { get; private set; } + [SerializeField] private GameObject settingPanel; // 변수를 선언할거에요 -> 설정 패널 오브젝트인 settingPanel을 + [SerializeField] private GameObject gameStopPanel; // 변수를 선언할거에요 -> 일시정지 패널 오브젝트인 gameStopPanel을 + [HideInInspector] public bool IsSettingOpen { get; private set; } // 프로퍼티를 선언할거에요 -> 설정창 열림 여부를 나타내는 IsSettingOpen을 (인스펙터에서는 숨김) - [Header("â г")] - [SerializeField] private GameObject normalPanel; - [SerializeField] private GameObject graphicPanel; + [Header("설정창 선택 패널")] // 인스펙터 헤더를 작성할거에요 -> "설정창 선택 패널"이라는 제목을 + [SerializeField] private GameObject normalPanel; // 변수를 선언할거에요 -> 일반 설정 패널인 normalPanel을 + [SerializeField] private GameObject graphicPanel; // 변수를 선언할거에요 -> 그래픽 설정 패널인 graphicPanel을 - [Header(" ݴ Ҹ")] - [SerializeField] private AudioSource audioSource; - [SerializeField] private AudioClip openSound; - [SerializeField] private AudioClip closeSound; + [Header("열고 닫는 소리")] // 인스펙터 헤더를 작성할거에요 -> "열고 닫는 소리"라는 제목을 + [SerializeField] private AudioSource audioSource; // 변수를 선언할거에요 -> 소리를 재생할 오디오 소스 컴포넌트를 + [SerializeField] private AudioClip openSound; // 변수를 선언할거에요 -> 열릴 때 재생할 오디오 클립을 + [SerializeField] private AudioClip closeSound; // 변수를 선언할거에요 -> 닫힐 때 재생할 오디오 클립을 - public void AllClose() + public void AllClose() // 함수를 선언할거에요 -> 모든 하위 패널을 닫는 AllClose를 { - normalPanel.SetActive(false); - graphicPanel.SetActive(false); + normalPanel.SetActive(false); // 기능을 껄거에요 -> 일반 설정 패널을 + graphicPanel.SetActive(false); // 기능을 껄거에요 -> 그래픽 설정 패널을 } - public void NormalOpen() + public void NormalOpen() // 함수를 선언할거에요 -> 일반 설정 패널을 여는 NormalOpen을 { - AllClose(); - normalPanel.SetActive(true); - if (audioSource != null && openSound != null) + AllClose(); // 함수를 실행할거에요 -> 모든 패널을 닫는 기능을 + normalPanel.SetActive(true); // 기능을 켤거에요 -> 일반 설정 패널을 + if (audioSource != null && openSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 열기 사운드가 있다면 { - audioSource.PlayOneShot(openSound); + audioSource.PlayOneShot(openSound); // 소리를 재생할거에요 -> 열기 효과음을 한 번 } } - public void GraphicOpen() + public void GraphicOpen() // 함수를 선언할거에요 -> 그래픽 설정 패널을 여는 GraphicOpen을 { - AllClose(); - graphicPanel.SetActive(true); - if (audioSource != null && openSound != null) + AllClose(); // 함수를 실행할거에요 -> 모든 패널을 닫는 기능을 + graphicPanel.SetActive(true); // 기능을 켤거에요 -> 그래픽 설정 패널을 + if (audioSource != null && openSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 열기 사운드가 있다면 { - audioSource.PlayOneShot(openSound); + audioSource.PlayOneShot(openSound); // 소리를 재생할거에요 -> 열기 효과음을 한 번 } } - public void OpenSetting() + public void OpenSetting() // 함수를 선언할거에요 -> 설정창 전체를 여는 OpenSetting을 { - IsSettingOpen = true; + IsSettingOpen = true; // 상태를 바꿀거에요 -> 설정창 열림 상태를 참(true)으로 - if (audioSource != null && openSound != null) + if (audioSource != null && openSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 열기 사운드가 있다면 { - audioSource.PlayOneShot(openSound); + audioSource.PlayOneShot(openSound); // 소리를 재생할거에요 -> 열기 효과음을 한 번 } - settingPanel.SetActive(true); + settingPanel.SetActive(true); // 기능을 켤거에요 -> 설정 패널 전체를 - if (gameStopPanel != null) + if (gameStopPanel != null) // 조건이 맞으면 실행할거에요 -> 일시정지 패널이 연결되어 있다면 { - gameStopPanel.SetActive(false); + gameStopPanel.SetActive(false); // 기능을 껄거에요 -> 일시정지 패널을 (설정창이랑 겹치지 않게) } } - public void CloseSetting() + public void CloseSetting() // 함수를 선언할거에요 -> 설정창을 닫는 CloseSetting을 { - IsSettingOpen = false; + IsSettingOpen = false; // 상태를 바꿀거에요 -> 설정창 열림 상태를 거짓(false)으로 - if (audioSource != null && closeSound != null) + if (audioSource != null && closeSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 닫기 사운드가 있다면 { - audioSource.PlayOneShot(closeSound); + audioSource.PlayOneShot(closeSound); // 소리를 재생할거에요 -> 닫기 효과음을 한 번 } - settingPanel.SetActive(false); + settingPanel.SetActive(false); // 기능을 껄거에요 -> 설정 패널 전체를 - if (gameStopPanel != null) + if (gameStopPanel != null) // 조건이 맞으면 실행할거에요 -> 일시정지 패널이 연결되어 있다면 { - gameStopPanel.SetActive(true); + gameStopPanel.SetActive(true); // 기능을 켤거에요 -> 일시정지 패널을 다시 보이게 } } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/ObjectPool/GenericObjectPool.cs b/Assets/Scripts/Systems/ObjectPool/GenericObjectPool.cs index 1d968f28..d35aea38 100644 --- a/Assets/Scripts/Systems/ObjectPool/GenericObjectPool.cs +++ b/Assets/Scripts/Systems/ObjectPool/GenericObjectPool.cs @@ -1,50 +1,50 @@ -using System.Collections.Generic; -using UnityEngine; +using System.Collections.Generic; // 리스트와 딕셔너리 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class GenericObjectPool : MonoBehaviour +public class GenericObjectPool : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 GenericObjectPool을 { - public static GenericObjectPool Instance; + public static GenericObjectPool Instance; // 변수를 선언할거에요 -> 싱글톤 인스턴스인 Instance를 - [System.Serializable] - public class Pool + [System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정 가능하게 Pool 클래스를 + public class Pool // 내부 클래스를 선언할거에요 -> 풀 정보를 담을 Pool을 { - public string tag; - public GameObject prefab; - public int size; + public string tag; // 변수를 선언할거에요 -> 풀의 태그 이름을 tag에 + public GameObject prefab; // 변수를 선언할거에요 -> 생성할 프리팹을 prefab에 + public int size; // 변수를 선언할거에요 -> 미리 생성할 개수를 size에 } - public List pools; - public Dictionary> poolDictionary; + public List pools; // 리스트를 선언할거에요 -> 설정된 풀 목록을 pools에 + public Dictionary> poolDictionary; // 딕셔너리를 선언할거에요 -> 태그별 오브젝트 큐를 관리할 poolDictionary를 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를 { - Instance = this; - poolDictionary = new Dictionary>(); + Instance = this; // 값을 저장할거에요 -> 내 자신을 싱글톤 인스턴스에 + poolDictionary = new Dictionary>(); // 초기화할거에요 -> 딕셔너리를 새로 생성해서 - foreach (Pool pool in pools) + foreach (Pool pool in pools) // 반복할거에요 -> 설정된 모든 풀 정보에 대해 { - Queue objectPool = new Queue(); - for (int i = 0; i < pool.size; i++) + Queue objectPool = new Queue(); // 큐를 생성할거에요 -> 오브젝트를 담을 큐를 + for (int i = 0; i < pool.size; i++) // 반복할거에요 -> 설정된 사이즈만큼 { - GameObject obj = Instantiate(pool.prefab); - obj.SetActive(false); // 꺼둔 상태로 보관 - objectPool.Enqueue(obj); + GameObject obj = Instantiate(pool.prefab); // 생성할거에요 -> 프리팹을 인스턴스로 + obj.SetActive(false); // 기능을 껄거에요 -> 생성된 오브젝트를 비활성화 상태로 (보관) + objectPool.Enqueue(obj); // 추가할거에요 -> 큐에 오브젝트를 } - poolDictionary.Add(pool.tag, objectPool); + poolDictionary.Add(pool.tag, objectPool); // 추가할거에요 -> 딕셔너리에 태그와 큐를 } } // ⭐ 몹을 꺼내 쓸 때 호출 (Instantiate 대신 사용) - public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation) + public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation) // 함수를 선언할거에요 -> 풀에서 오브젝트를 꺼내는 SpawnFromPool을 { - if (!poolDictionary.ContainsKey(tag)) return null; + if (!poolDictionary.ContainsKey(tag)) return null; // 조건이 맞으면 반환할거에요 -> 태그가 없다면 null을 - GameObject objectToSpawn = poolDictionary[tag].Dequeue(); - objectToSpawn.SetActive(true); - objectToSpawn.transform.position = position; - objectToSpawn.transform.rotation = rotation; + GameObject objectToSpawn = poolDictionary[tag].Dequeue(); // 꺼낼거에요 -> 큐의 맨 앞 오브젝트를 + objectToSpawn.SetActive(true); // 기능을 켤거에요 -> 오브젝트를 활성화해서 + objectToSpawn.transform.position = position; // 위치를 설정할거에요 -> 지정된 위치로 + objectToSpawn.transform.rotation = rotation; // 회전을 설정할거에요 -> 지정된 회전으로 - poolDictionary[tag].Enqueue(objectToSpawn); // 다시 큐의 끝으로 보냄 - return objectToSpawn; + poolDictionary[tag].Enqueue(objectToSpawn); // 추가할거에요 -> 다시 큐의 맨 뒤로 (재사용 대기) + return objectToSpawn; // 반환할거에요 -> 꺼낸 오브젝트를 } } \ No newline at end of file diff --git a/Assets/Scripts/Systems/Optimization/Editor/PlayerRangeManagerEditor.cs b/Assets/Scripts/Systems/Optimization/Editor/PlayerRangeManagerEditor.cs index fcb9d5ee..a8cc9ec8 100644 --- a/Assets/Scripts/Systems/Optimization/Editor/PlayerRangeManagerEditor.cs +++ b/Assets/Scripts/Systems/Optimization/Editor/PlayerRangeManagerEditor.cs @@ -1,73 +1,73 @@ -using UnityEngine; -using UnityEditor; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEditor; // 유니티 에디터 기능을 사용할거에요 -> UnityEditor를 -namespace GameSystems.Optimization.Editor +namespace GameSystems.Optimization.Editor // 에디터 네임스페이스를 정의할거에요 -> GameSystems.Optimization.Editor로 { - [CustomEditor(typeof(PlayerRangeManager))] - public sealed class PlayerRangeManagerEditor : UnityEditor.Editor + [CustomEditor(typeof(PlayerRangeManager))] // 커스텀 에디터 대상을 지정할거에요 -> PlayerRangeManager 클래스로 + public sealed class PlayerRangeManagerEditor : UnityEditor.Editor // 클래스를 선언할거에요 -> 에디터 상속을 받는 PlayerRangeManagerEditor를 { - private SerializedProperty _configProperty; - private SerializedProperty _parentsProperty; // ⭐ 변수명 일치 + private SerializedProperty _configProperty; // 변수를 선언할거에요 -> 설정 속성을 연결할 _configProperty를 + private SerializedProperty _parentsProperty; // 변수를 선언할거에요 -> 부모 목록 속성을 연결할 _parentsProperty를 - private void OnEnable() + private void OnEnable() // 에디터가 활성화될 때 실행할거에요 -> OnEnable 함수를 { - _configProperty = serializedObject.FindProperty("_config"); + _configProperty = serializedObject.FindProperty("_config"); // 찾을거에요 -> 대상의 "_config" 변수를 // ⭐ [에러 해결] 리스트 이름 '_environmentParents'를 정확히 찾아옵니다. - _parentsProperty = serializedObject.FindProperty("_environmentParents"); + _parentsProperty = serializedObject.FindProperty("_environmentParents"); // 찾을거에요 -> 대상의 "_environmentParents" 변수를 } - public override void OnInspectorGUI() + public override void OnInspectorGUI() // 인스펙터 화면을 그릴거에요 -> OnInspectorGUI 함수를 { - PlayerRangeManager manager = (PlayerRangeManager)target; - serializedObject.Update(); + PlayerRangeManager manager = (PlayerRangeManager)target; // 변수를 설정할거에요 -> 현재 선택된 매니저 객체로 + serializedObject.Update(); // 실행할거에요 -> 객체의 최신 데이터를 불러오기를 // === 헤더 === - DrawMainHeader(); // ⭐ [경고 해결] DrawHeader 이름을 DrawMainHeader로 변경 + DrawMainHeader(); // 함수를 실행할거에요 -> 메인 헤더를 그리는 - EditorGUILayout.Space(5); + EditorGUILayout.Space(5); // 공간을 줄거에요 -> 5픽셀만큼의 여백을 // === 설정 영역 === - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - GUILayout.Label("필수 설정", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(_configProperty); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요 -> 헬프박스 스타일로 + GUILayout.Label("필수 설정", EditorStyles.boldLabel); // 라벨을 표시할거에요 -> "필수 설정"이라는 제목을 + EditorGUILayout.PropertyField(_configProperty); // 그려낼거에요 -> 설정 에셋 입력 필드를 // ⭐ [에러 해결] 리스트를 안전하게 그립니다. - if (_parentsProperty != null) + if (_parentsProperty != null) // 조건이 맞으면 실행할거에요 -> 부모 속성이 유효하다면 { - EditorGUILayout.PropertyField(_parentsProperty, new GUIContent("Environment Parents"), true); + EditorGUILayout.PropertyField(_parentsProperty, new GUIContent("Environment Parents"), true); // 그려낼거에요 -> 자식 요소까지 포함한 리스트 필드를 } - EditorGUILayout.EndVertical(); + EditorGUILayout.EndVertical(); // 수직 영역을 종료할거에요 // === 실시간 통계 === - if (Application.isPlaying && manager.IsInitialized) + if (Application.isPlaying && manager.IsInitialized) // 조건이 맞으면 실행할거에요 -> 게임 중이고 초기화되었다면 { - EditorGUILayout.Space(5); - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - GUILayout.Label("실시간 통계", EditorStyles.boldLabel); - EditorGUILayout.LabelField("총 오브젝트:", manager.TotalObjectCount.ToString()); - EditorGUILayout.LabelField("표시 중:", manager.VisibleObjectCount.ToString()); - EditorGUILayout.LabelField("컬링 효율:", $"{(manager.CullingEfficiency * 100f):F1}%"); - EditorGUILayout.EndVertical(); + EditorGUILayout.Space(5); // 여백을 줄거에요 + EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요 + GUILayout.Label("실시간 통계", EditorStyles.boldLabel); // 제목을 표시할거에요 -> "실시간 통계"를 + EditorGUILayout.LabelField("총 오브젝트:", manager.TotalObjectCount.ToString()); // 텍스트를 표시할거에요 -> 총 개수 정보를 + EditorGUILayout.LabelField("표시 중:", manager.VisibleObjectCount.ToString()); // 텍스트를 표시할거에요 -> 현재 표시 개수 정보를 + EditorGUILayout.LabelField("컬링 효율:", $"{(manager.CullingEfficiency * 100f):F1}%"); // 텍스트를 표시할거에요 -> 효율 퍼센트 정보를 + EditorGUILayout.EndVertical(); // 수직 영역을 종료할거에요 } - EditorGUILayout.Space(5); + EditorGUILayout.Space(5); // 여백을 줄거에요 // === 컨트롤 버튼 === - if (GUILayout.Button("🔄 오브젝트 목록 새로고침", GUILayout.Height(30))) + if (GUILayout.Button("🔄 오브젝트 목록 새로고침", GUILayout.Height(30))) // 조건이 맞으면 실행할거에요 -> 버튼을 눌렀다면 { - if (Application.isPlaying) manager.RefreshObjectList(); + if (Application.isPlaying) manager.RefreshObjectList(); // 실행할거에요 -> 실행 중일 때 목록 갱신 기능을 } - serializedObject.ApplyModifiedProperties(); - if (Application.isPlaying) Repaint(); + serializedObject.ApplyModifiedProperties(); // 실행할거에요 -> 변경된 데이터를 실제 객체에 반영하기를 + if (Application.isPlaying) Repaint(); // 실행할거에요 -> 실행 중일 때 화면을 다시 그리기를 } - private void DrawMainHeader() // ⭐ 이름 변경됨 + private void DrawMainHeader() // 함수를 선언할거에요 -> 메인 헤더를 그리는 DrawMainHeader를 { - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - GUILayout.Label("Player Range Manager", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, alignment = TextAnchor.MiddleCenter }); - EditorGUILayout.EndVertical(); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요 + GUILayout.Label("Player Range Manager", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, alignment = TextAnchor.MiddleCenter }); // 큰 제목을 그릴거에요 -> 중앙 정렬된 스타일로 + EditorGUILayout.EndVertical(); // 수직 영역을 종료할거에요 } } } \ No newline at end of file diff --git a/Assets/Scripts/Systems/Optimization/Player Render Manager.cs b/Assets/Scripts/Systems/Optimization/Player Render Manager.cs index 4459f9b5..647f3aa9 100644 --- a/Assets/Scripts/Systems/Optimization/Player Render Manager.cs +++ b/Assets/Scripts/Systems/Optimization/Player Render Manager.cs @@ -1,77 +1,68 @@ -using UnityEngine; -using System.Collections.Generic; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 -public class PlayerRangeManager : MonoBehaviour +public class PlayerRangeManager : MonoBehaviour // 클래스를 선언할거에요 -> 거리 기반 최적화 컴포넌트인 PlayerRangeManager를 { - [Header("--- 대상 설정 ---")] - [SerializeField] private Transform environmentParent; // 맵 오브젝트 부모 폴더 + [Header("--- 대상 설정 ---")] // 제목을 표시할거에요 -> --- 대상 설정 --- 을 + [SerializeField] private Transform environmentParent; // 변수를 선언할거에요 -> 맵 오브젝트들이 있는 부모 폴더를 - [Header("--- 최적화 설정 ---")] - [SerializeField] private float renderRange = 40f; // ⭐ 반드시 30~50 사이로 키워주세요! - [SerializeField] private float checkInterval = 0.2f; + [Header("--- 최적화 설정 ---")] // 제목을 표시할거에요 -> --- 최적화 설정 --- 을 + [SerializeField] private float renderRange = 40f; // 변수를 선언할거에요 -> 표시 사거리인 40을 renderRange에 + [SerializeField] private float checkInterval = 0.2f; // 변수를 선언할거에요 -> 검사 주기인 0.2를 checkInterval에 - private struct RenderGroup + private struct RenderGroup // 구조체를 정의할거에요 -> 렌더링 그룹 데이터 구조를 { - public Vector3 actualCenter; // ⭐ 피벗이 아닌 실제 메쉬의 중심점 - public Renderer[] renderers; + public Vector3 actualCenter; // 변수를 선언할거에요 -> 실제 메쉬의 중심 위치를 + public Renderer[] renderers; // 배열을 선언할거에요 -> 포함된 렌더러 목록을 } - private List _childGroups = new List(); - private Transform _player; + private List _childGroups = new List(); // 리스트를 생성할거에요 -> 하위 그룹들을 관리할 목록을 + private Transform _player; // 변수를 선언할거에요 -> 플레이어 위치 정보를 담을 변수를 - private void Start() + private void Start() // 함수를 실행할거에요 -> 시작 시 Start를 { // 태그로 플레이어를 정확히 찾습니다. - GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); - if (playerObj != null) _player = playerObj.transform; - else _player = transform; + GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); // 찾을거에요 -> "Player" 태그를 가진 오브젝트를 + if (playerObj != null) _player = playerObj.transform; // 조건이 맞으면 저장할거에요 -> 플레이어 위치를 + else _player = transform; // 조건이 틀리면 저장할거에요 -> 내 위치를 대신 - if (environmentParent != null) RefreshChildList(); - InvokeRepeating(nameof(UpdateCulling), 0f, checkInterval); + if (environmentParent != null) RefreshChildList(); // 조건이 맞으면 실행할거에요 -> 환경 오브젝트 목록 갱신을 + InvokeRepeating(nameof(UpdateCulling), 0f, checkInterval); // 반복 실행할거에요 -> 컬링 업데이트 함수를 주기적으로 } - public void RefreshChildList() + public void RefreshChildList() // 함수를 선언할거에요 -> 자식 목록을 새로고침하는 RefreshChildList를 { - if (environmentParent == null) return; - _childGroups.Clear(); + if (environmentParent == null) return; // 조건이 맞으면 중단할거에요 -> 부모 폴더가 없다면 + _childGroups.Clear(); // 리스트를 비울거에요 -> 기존 목록을 - foreach (Transform child in environmentParent) + foreach (Transform child in environmentParent) // 반복할거에요 -> 부모 내의 모든 자식 오브젝트에 대해 { - Renderer[] rs = child.GetComponentsInChildren(true); - if (rs.Length > 0) + Renderer[] rs = child.GetComponentsInChildren(true); // 배열을 가져올거에요 -> 자식 내부의 모든 렌더러들을 + if (rs.Length > 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재한다면 { // ⭐ [수정] 피벗(position)이 아니라, 실제 모델링의 중심(bounds.center)을 가져옵니다. // 이래야 "가까이 갔는데 꺼지는" 현상이 완벽히 해결됩니다! - Vector3 center = rs[0].bounds.center; - _childGroups.Add(new RenderGroup { actualCenter = center, renderers = rs }); + Vector3 center = rs[0].bounds.center; // 위치를 가져올거에요 -> 첫 번째 렌더러의 실제 경계 중심점을 + _childGroups.Add(new RenderGroup { actualCenter = center, renderers = rs }); // 리스트에 추가할거에요 -> 새로운 그룹 정보를 생성해서 } } } - private void UpdateCulling() + private void UpdateCulling() // 함수를 선언할거에요 -> 실제 컬링 로직인 UpdateCulling을 { - if (_player == null) return; - Vector3 playerPos = _player.position; + if (_player == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 없다면 + Vector3 playerPos = _player.position; // 값을 가져올거에요 -> 현재 플레이어의 위치를 - foreach (var group in _childGroups) + foreach (var group in _childGroups) // 반복할거에요 -> 모든 관리 그룹에 대해 { // ⭐ [핵심] 실제 몸통 중심점과의 거리를 계산합니다. - float dist = Vector3.Distance(playerPos, group.actualCenter); - bool shouldShow = dist <= renderRange; + float dist = Vector3.Distance(playerPos, group.actualCenter); // 거리를 계산할거에요 -> 플레이어와 그룹 중심 사이의 + bool shouldShow = dist <= renderRange; // 상태를 결정할거에요 -> 범위 이내인지 여부를 - foreach (Renderer r in group.renderers) + foreach (Renderer r in group.renderers) // 반복할거에요 -> 그룹 내 모든 렌더러에 대해 { - if (r != null && r.enabled != shouldShow) r.enabled = shouldShow; + if (r != null && r.enabled != shouldShow) r.enabled = shouldShow; // 조건이 맞으면 상태를 바꿀거에요 -> 렌더러의 활성화 여부를 } } } - - private void OnDrawGizmos() - { - if (_player != null) - { - Gizmos.color = Color.red; - Gizmos.DrawWireSphere(_player.position, renderRange); // 플레이어를 따라다니는 빨간 원 - } - } } \ No newline at end of file diff --git a/Assets/Scripts/Systems/Optimization/PlayerRangeManager.cs b/Assets/Scripts/Systems/Optimization/PlayerRangeManager.cs index 94699790..b9133f5f 100644 --- a/Assets/Scripts/Systems/Optimization/PlayerRangeManager.cs +++ b/Assets/Scripts/Systems/Optimization/PlayerRangeManager.cs @@ -1,151 +1,157 @@ -using UnityEngine; -using System.Collections.Generic; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 -namespace GameSystems.Optimization +namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로 { - [DisallowMultipleComponent] - [AddComponentMenu("Game Systems/Optimization/Player Range Manager")] - public sealed class PlayerRangeManager : MonoBehaviour + [DisallowMultipleComponent] // 기능을 제한할거에요 -> 중복 부착을 방지하도록 + [AddComponentMenu("Game Systems/Optimization/Player Range Manager")] // 메뉴를 추가할거에요 -> 컴포넌트 추가 경로에 + public sealed class PlayerRangeManager : MonoBehaviour // 클래스를 선언할거에요 -> 최적화 관리자인 PlayerRangeManager를 { #region Serialized Fields - [Header("=== 필수 설정 ===")] - [SerializeField, Tooltip("최적화 설정 에셋")] - private RenderOptimizationConfig _config; + [Header("=== 필수 설정 ===")] // 제목을 표시할거에요 -> === 필수 설정 === 을 + [SerializeField, Tooltip("최적화 설정 에셋")] // 필드를 직렬화할거에요 -> 인스펙터 노출을 위해 + private RenderOptimizationConfig _config; // 변수를 선언할거에요 -> 설정 에셋을 담을 _config를 - [SerializeField, Tooltip("환경 오브젝트들이 들어있는 부모 폴더 리스트")] - private List _environmentParents = new List(); // ⭐ 리스트 방식으로 변경 + [SerializeField, Tooltip("환경 오브젝트들이 들어있는 부모 폴더 리스트")] // 툴팁을 추가할거에요 -> 설명 문구를 + private List _environmentParents = new List(); // 리스트를 생성할거에요 -> 부모 폴더들을 관리할 목록을 #endregion #region Private Fields - private Transform _playerTransform; - private readonly List _renderGroups = new List(512); + private Transform _playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 저장할 변수를 + private readonly List _renderGroups = new List(512); // 리스트를 생성할거에요 -> 렌더 그룹들을 담을 목록을 - private bool _isInitialized; - private bool _isPaused; + private bool _isInitialized; // 변수를 선언할거에요 -> 초기화 완료 여부를 + private bool _isPaused; // 변수를 선언할거에요 -> 시스템 일시정지 여부를 - private int _totalObjectCount; - private int _visibleObjectCount; - private int _lastFrameChangedCount; - private float _nextCheckTime; + private int _totalObjectCount; // 변수를 선언할거에요 -> 관리 대상 총 개수를 + private int _visibleObjectCount; // 변수를 선언할거에요 -> 화면에 표시 중인 개수를 + private int _lastFrameChangedCount; // 변수를 선언할거에요 -> 지난 프레임의 상태 변화 수를 + private float _nextCheckTime; // 변수를 선언할거에요 -> 다음 검사 시간을 #endregion #region Public Properties - public bool IsInitialized => _isInitialized; - public bool IsPaused => _isPaused; - public int TotalObjectCount => _totalObjectCount; - public int VisibleObjectCount => _visibleObjectCount; - public float CullingEfficiency => _totalObjectCount > 0 ? 1f - ((float)_visibleObjectCount / _totalObjectCount) : 0f; + public bool IsInitialized => _isInitialized; // 값을 반환할거에요 -> 초기화 완료 여부 상태를 + public bool IsPaused => _isPaused; // 값을 반환할거에요 -> 일시정지 여부 상태를 + public int TotalObjectCount => _totalObjectCount; // 값을 반환할거에요 -> 총 관리 개수 수치를 + public int VisibleObjectCount => _visibleObjectCount; // 값을 반환할거에요 -> 현재 표시 개수 수치를 + public float CullingEfficiency => _totalObjectCount > 0 ? 1f - ((float)_visibleObjectCount / _totalObjectCount) : 0f; // 값을 계산해서 반환할거에요 -> 컬링 효율 비율을 #endregion - private void Awake() { ValidateConfiguration(); } - private void Start() { InitializeSystem(); } + private void Awake() { ValidateConfiguration(); } // 함수를 실행할거에요 -> 시작 시 설정을 검증하는 Awake를 + private void Start() { InitializeSystem(); } // 함수를 실행할거에요 -> 시스템을 초기화하는 Start를 - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트 로직인 Update를 { - if (!_isInitialized || _isPaused) return; - if (Time.time >= _nextCheckTime) + if (!_isInitialized || _isPaused || _playerTransform == null) return; // 조건이 맞으면 중단할거에요 -> 동작 불가능한 상태라면 + + if (Time.time >= _nextCheckTime) // 조건이 맞으면 실행할거에요 -> 다음 검사 시간이 되었다면 { - UpdateCulling(); - _nextCheckTime = Time.time + GetCheckInterval(); + UpdateCulling(); // 함수를 실행할거에요 -> 컬링 업데이트 기능을 + _nextCheckTime = Time.time + GetCheckInterval(); // 값을 갱신할거에요 -> 다음 검사 예정 시간을 } } - private void ValidateConfiguration() + private void ValidateConfiguration() // 함수를 선언할거에요 -> 설정 누락 여부를 확인하는 ValidateConfiguration을 { - if (_config == null) _config = RenderOptimizationConfig.CreateDefault(); - } - - public void InitializeSystem() - { - if (!FindPlayer()) return; - if (!ScanEnvironmentObjects()) return; - - _isInitialized = true; - _nextCheckTime = Time.time; - } - - private bool FindPlayer() - { - GameObject playerObj = GameObject.FindGameObjectWithTag(GetPlayerTag()); - _playerTransform = (playerObj != null) ? playerObj.transform : transform; - return true; - } - - // ⭐ [핵심 수정] 리스트 내의 모든 폴더를 뒤져서 '낱개' 단위로 등록합니다. - private bool ScanEnvironmentObjects() - { - if (_environmentParents == null || _environmentParents.Count == 0) return false; - - _renderGroups.Clear(); - - foreach (Transform parent in _environmentParents) + if (_config == null) // 조건이 맞으면 실행할거에요 -> 설정 에셋이 비어있다면 { - if (parent == null) continue; + Debug.LogWarning("[PlayerRangeManager] Config가 없습니다! 기본값을 생성합니다."); // 경고를 출력할거에요 -> 자동 생성 메시지를 + _config = RenderOptimizationConfig.CreateDefault(); // 값을 설정할거에요 -> 기본값으로 생성된 에셋을 + } + } - // ⭐ 폴더 구조가 몇 층이든 상관없이 모든 Renderer를 낱낱이 찾아냅니다. - Renderer[] allRenderers = parent.GetComponentsInChildren(_config.IncludeInactiveObjects); + private void InitializeSystem() // 함수를 선언할거에요 -> 전체 시스템을 초기화하는 InitializeSystem을 + { + FindPlayer(); // 함수를 실행할거에요 -> 플레이어를 찾는 기능을 + ScanEnvironmentObjects(); // 함수를 실행할거에요 -> 주변 오브젝트를 스캔하는 기능을 - foreach (Renderer r in allRenderers) + _isInitialized = true; // 상태를 바꿀거에요 -> 초기화 완료인 참(true)으로 + if (ShouldLog()) Debug.Log($"[PlayerRangeManager] 초기화 완료. 대상 그룹: {_totalObjectCount}"); // 조건이 맞으면 로그를 출력할거에요 -> 결과 보고 메시지를 + } + + private void FindPlayer() // 함수를 선언할거에요 -> 플레이어를 탐색하는 FindPlayer를 + { + GameObject player = GameObject.FindGameObjectWithTag(GetPlayerTag()); // 오브젝트를 찾을거에요 -> 지정된 태그를 가진 플레이어를 + if (player != null) // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면 + { + _playerTransform = player.transform; // 값을 저장할거에요 -> 플레이어의 Transform 정보를 + } + else // 조건이 틀리면 실행할거에요 -> 플레이어를 못 찾았다면 + { + Debug.LogError($"[PlayerRangeManager] '{GetPlayerTag()}' 태그의 플레이어를 찾을 수 없습니다!"); // 에러를 출력할거에요 -> 태그 불일치 메시지를 + } + } + + private void ScanEnvironmentObjects() // 함수를 선언할거에요 -> 환경 오브젝트들을 스캔하는 ScanEnvironmentObjects를 + { + _renderGroups.Clear(); // 리스트를 비울거에요 -> 기존에 있던 렌더 그룹들을 + + foreach (var parent in _environmentParents) // 반복할거에요 -> 등록된 모든 부모 폴더에 대해 + { + if (parent == null) continue; // 조건이 맞으면 건너뛸거에요 -> 부모가 비어있다면 + + foreach (Transform child in parent) // 반복할거에요 -> 부모 폴더의 모든 자식 오브젝트에 대해 { - if (r == null) continue; - // 각 렌더러를 독립적인 그룹으로 생성 (개별 최적화의 핵심) - _renderGroups.Add(new RenderGroup(new Renderer[] { r })); + Renderer[] renderers = child.GetComponentsInChildren(true); // 배열을 가져올거에요 -> 자식 내부의 모든 렌더러들을 + if (renderers.Length > 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재한다면 + { + _renderGroups.Add(new RenderGroup(renderers)); // 리스트에 추가할거에요 -> 새로운 렌더 그룹을 생성해서 + } } } - _totalObjectCount = _renderGroups.Count; - return _totalObjectCount > 0; + _totalObjectCount = _renderGroups.Count; // 값을 저장할거에요 -> 총 관리 대상 개수를 } - private void UpdateCulling() + private void UpdateCulling() // 함수를 선언할거에요 -> 컬링을 업데이트하는 UpdateCulling을 { - if (_playerTransform == null) return; + Vector3 playerPos = _playerTransform.position; // 값을 가져올거에요 -> 현재 플레이어의 위치를 + float range = GetRenderRange(); // 값을 가져올거에요 -> 설정된 렌더링 거리 수치를 - Vector3 playerPosition = _playerTransform.position; - float maxDistance = GetRenderRange(); - int changedCount = 0; - int visibleCount = 0; + int changedCount = 0; // 변수를 초기화할거에요 -> 변경 개수를 0으로 + int visibleCount = 0; // 변수를 초기화할거에요 -> 표시 중인 개수를 0으로 - for (int i = 0; i < _renderGroups.Count; i++) + for (int i = 0; i < _renderGroups.Count; i++) // 반복할거에요 -> 모든 렌더 그룹에 대해 { - if (_renderGroups[i].UpdateVisibility(playerPosition, maxDistance)) changedCount++; - if (_renderGroups[i].IsVisible) visibleCount++; + if (_renderGroups[i].UpdateVisibility(playerPos, range)) changedCount++; // 조건이 맞으면 증가시킬거에요 -> 상태가 바뀌었다면 변경 수를 + if (_renderGroups[i].IsVisible) visibleCount++; // 조건이 맞으면 증가시킬거에요 -> 현재 보이고 있다면 표시 수를 } - _visibleObjectCount = visibleCount; - _lastFrameChangedCount = changedCount; + _visibleObjectCount = visibleCount; // 값을 저장할거에요 -> 이번 검사 결과의 총 표시 개수를 + _lastFrameChangedCount = changedCount; // 값을 저장할거에요 -> 변화가 발생한 그룹 수를 } - public void RefreshObjectList() { ScanEnvironmentObjects(); } - public void ShowAllObjects() { foreach (var g in _renderGroups) g.ForceShow(); _visibleObjectCount = _totalObjectCount; } - public void HideAllObjects() { foreach (var g in _renderGroups) g.ForceHide(); _visibleObjectCount = 0; } - public void SetPaused(bool paused) => _isPaused = paused; + public void RefreshObjectList() { ScanEnvironmentObjects(); } // 함수를 실행할거에요 -> 목록을 새로 스캔하는 기능을 + public void ShowAllObjects() { foreach (var g in _renderGroups) g.ForceShow(); _visibleObjectCount = _totalObjectCount; } // 함수를 실행할거에요 -> 모든 대상을 켜는 기능을 + public void HideAllObjects() { foreach (var g in _renderGroups) g.ForceHide(); _visibleObjectCount = 0; } // 함수를 실행할거에요 -> 모든 대상을 끄는 기능을 + public void SetPaused(bool paused) => _isPaused = paused; // 값을 바꿀거에요 -> 일시정지 여부를 전달받은 인자대로 - private float GetRenderRange() => _config.RenderRange; - private float GetCheckInterval() => _config.CheckInterval; - private string GetPlayerTag() => _config.PlayerTag; - private bool ShouldShowGizmos() => _config.ShowGizmos; + private float GetRenderRange() => _config.RenderRange; // 값을 가져올거에요 -> 설정 파일의 렌더링 사거리 수치를 + private float GetCheckInterval() => _config.CheckInterval; // 값을 가져올거에요 -> 설정 파일의 검사 간격 수치를 + private string GetPlayerTag() => _config.PlayerTag; // 값을 가져올거에요 -> 설정 파일의 플레이어 태그를 + private bool ShouldShowGizmos() => _config.ShowGizmos; // 값을 가져올거에요 -> 설정 파일의 기즈모 표시 여부를 + private bool ShouldLog() => _config.EnableVerboseLogging; // 값을 가져올거에요 -> 설정 파일의 로그 활성화 여부를 - private void OnDrawGizmos() + private void OnDrawGizmos() // 함수를 실행할거에요 -> 기즈모를 그리는 OnDrawGizmos를 { - if (!ShouldShowGizmos() || _playerTransform == null) return; - Gizmos.color = new Color(1f, 0f, 0f, 0.3f); - Gizmos.DrawWireSphere(_playerTransform.position, GetRenderRange()); + if (!ShouldShowGizmos() || _playerTransform == null) return; // 조건이 맞으면 중단할거에요 -> 표시 설정이 꺼졌거나 플레이어가 없다면 + Gizmos.color = new Color(1f, 0f, 0f, 0.3f); // 색상을 설정할거에요 -> 빨간색 반투명으로 + Gizmos.DrawWireSphere(_playerTransform.position, GetRenderRange()); // 그림을 그릴거에요 -> 플레이어 중심의 사거리 구체를 } - private void OnDrawGizmosSelected() + private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 선택 시 기즈모를 그리는 OnDrawGizmosSelected를 { - if (!ShouldShowGizmos() || !_isInitialized) return; - foreach (var group in _renderGroups) + if (!ShouldShowGizmos() || !_isInitialized) return; // 조건이 맞으면 중단할거에요 -> 표시 설정이 꺼졌거나 초기화 전이라면 + foreach (var group in _renderGroups) // 반복할거에요 -> 모든 렌더 그룹에 대해 { - Gizmos.color = group.IsVisible ? new Color(0f, 1f, 0f, 0.5f) : new Color(0.5f, 0.5f, 0.5f, 0.2f); - Gizmos.DrawSphere(group.ActualCenter, 0.3f); + Gizmos.color = group.IsVisible ? new Color(0f, 1f, 0f, 0.5f) : new Color(0.5f, 0.5f, 0.5f, 0.2f); // 색상을 결정할거에요 -> 상태에 따라 초록 혹은 회색으로 + Gizmos.DrawWireSphere(group.ActualCenter, group.BoundingRadius); // 그림을 그릴거에요 -> 각 그룹의 경계면 구체를 } } } diff --git a/Assets/Scripts/Systems/Optimization/RenderGroup.cs b/Assets/Scripts/Systems/Optimization/RenderGroup.cs index 993bdffa..a686a9f6 100644 --- a/Assets/Scripts/Systems/Optimization/RenderGroup.cs +++ b/Assets/Scripts/Systems/Optimization/RenderGroup.cs @@ -1,21 +1,21 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -namespace GameSystems.Optimization +namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로 { /// /// 렌더링 최적화를 위한 오브젝트 그룹 데이터 /// - 불변성(Immutability) 보장 /// - 캡슐화된 내부 데이터 /// - public sealed class RenderGroup + public sealed class RenderGroup // 클래스를 선언할거에요 -> 상속 불가능한 최적화 그룹인 RenderGroup을 { #region Private Fields - - private readonly Vector3 _actualCenter; - private readonly Renderer[] _renderers; - private readonly float _boundingRadius; - private bool _isCurrentlyVisible; - + + private readonly Vector3 _actualCenter; // 변수를 선언할거에요 -> 읽기 전용으로 실제 중심 좌표를 + private readonly Renderer[] _renderers; // 배열을 선언할거에요 -> 읽기 전용으로 렌더러 목록을 + private readonly float _boundingRadius; // 변수를 선언할거에요 -> 읽기 전용으로 경계 반지름 수치를 + private bool _isCurrentlyVisible; // 변수를 선언할거에요 -> 현재 표시 상태를 저장할 변수를 + #endregion #region Public Properties (Read-Only) @@ -23,22 +23,22 @@ namespace GameSystems.Optimization /// /// 실제 메쉬의 중심점 (피벗이 아닌 bounds 기준) /// - public Vector3 ActualCenter => _actualCenter; + public Vector3 ActualCenter => _actualCenter; // 프로퍼티를 선언할거에요 -> 실제 중심점을 반환하는 기능을 /// /// 오브젝트의 최대 반지름 (경계 구체) /// - public float BoundingRadius => _boundingRadius; + public float BoundingRadius => _boundingRadius; // 프로퍼티를 선언할거에요 -> 경계 반지름을 반환하는 기능을 /// /// 현재 렌더링 상태 /// - public bool IsVisible => _isCurrentlyVisible; + public bool IsVisible => _isCurrentlyVisible; // 프로퍼티를 선언할거에요 -> 표시 여부 상태를 반환하는 기능을 /// /// 관리 중인 Renderer 개수 /// - public int RendererCount => _renderers?.Length ?? 0; + public int RendererCount => _renderers?.Length ?? 0; // 프로퍼티를 선언할거에요 -> 렌더러 개수를 반환하는 기능을 (null이면 0) #endregion @@ -48,28 +48,28 @@ namespace GameSystems.Optimization /// RenderGroup 생성자 /// /// 대상 Renderer 배열 (null 체크 수행) - public RenderGroup(Renderer[] renderers) + public RenderGroup(Renderer[] renderers) // 생성자를 실행할거에요 -> 렌더러 배열을 인자로 받아서 { - if (renderers == null || renderers.Length == 0) + if (renderers == null || renderers.Length == 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 비어있거나 없다면 { - Debug.LogError("[RenderGroup] Renderer 배열이 null 또는 비어있습니다."); - _renderers = System.Array.Empty(); - _actualCenter = Vector3.zero; - _boundingRadius = 0f; - return; + Debug.LogError("[RenderGroup] Renderer 배열이 null 또는 비어있습니다."); // 에러를 출력할거에요 -> 로그 메시지를 + _renderers = System.Array.Empty(); // 값을 할당할거에요 -> 빈 렌더러 배열을 + _actualCenter = Vector3.zero; // 값을 할당할거에요 -> 제로 벡터를 중심점으로 + _boundingRadius = 0f; // 값을 할당할거에요 -> 0을 반지름으로 + return; // 중단할거에요 -> 초기화 로직을 } // 방어적 복사 (외부에서 배열 수정 방지) - _renderers = new Renderer[renderers.Length]; - System.Array.Copy(renderers, _renderers, renderers.Length); + _renderers = new Renderer[renderers.Length]; // 배열을 생성할거에요 -> 같은 크기의 새로운 렌더러 배열을 + System.Array.Copy(renderers, _renderers, renderers.Length); // 값을 복사할거에요 -> 원본 렌더러들을 내부 배열로 // 실제 중심점 계산 - _actualCenter = CalculateGroupCenter(_renderers); - + _actualCenter = CalculateGroupCenter(_renderers); // 값을 계산해서 넣을거에요 -> 그룹의 통합 중심점을 + // 경계 반지름 계산 - _boundingRadius = CalculateBoundingRadius(_renderers, _actualCenter); - - _isCurrentlyVisible = true; // 초기 상태는 표시 + _boundingRadius = CalculateBoundingRadius(_renderers, _actualCenter); // 값을 계산해서 넣을거에요 -> 최대 경계 반지름을 + + _isCurrentlyVisible = true; // 초기값을 설정할거에요 -> 표시 상태를 참(true)으로 } #endregion @@ -82,58 +82,58 @@ namespace GameSystems.Optimization /// 기준 위치 (플레이어 등) /// 최대 렌더링 거리 /// 상태가 변경되었는지 여부 - public bool UpdateVisibility(Vector3 referencePosition, float maxDistance) + public bool UpdateVisibility(Vector3 referencePosition, float maxDistance) // 함수를 선언할거에요 -> 거리에 따른 표시 갱신 기능을 { // 거리 계산 (오브젝트 크기 고려) - float distance = Vector3.Distance(referencePosition, _actualCenter); - bool shouldBeVisible = (distance - _boundingRadius) <= maxDistance; + float distance = Vector3.Distance(referencePosition, _actualCenter); // 거리를 계산할거에요 -> 기준점과 그룹 중심 사이의 + bool shouldBeVisible = (distance - _boundingRadius) <= maxDistance; // 상태를 판단할거에요 -> 사거리와 반지름을 고려해서 표시 여부를 // 상태 변경이 필요한 경우에만 처리 - if (shouldBeVisible == _isCurrentlyVisible) + if (shouldBeVisible == _isCurrentlyVisible) // 조건이 맞으면 실행할거에요 -> 이전 상태와 동일하다면 { - return false; // 변경 없음 + return false; // 반환할거에요 -> 변경되지 않았음을 } // Renderer 상태 일괄 변경 - SetRenderersState(shouldBeVisible); - _isCurrentlyVisible = shouldBeVisible; - - return true; // 변경됨 + SetRenderersState(shouldBeVisible); // 함수를 실행할거에요 -> 모든 렌더러의 활성화 상태를 바꾸는 기능을 + _isCurrentlyVisible = shouldBeVisible; // 값을 갱신할거에요 -> 현재 표시 상태를 + + return true; // 반환할거에요 -> 상태가 변경되었음을 } /// /// 강제로 모든 Renderer 표시 /// - public void ForceShow() + public void ForceShow() // 함수를 선언할거에요 -> 강제 표시 기능인 ForceShow를 { - SetRenderersState(true); - _isCurrentlyVisible = true; + SetRenderersState(true); // 함수를 실행할거에요 -> 모든 렌더러를 켜는 기능을 + _isCurrentlyVisible = true; // 상태를 설정할거에요 -> 표시 중인 것으로 } /// /// 강제로 모든 Renderer 숨김 /// - public void ForceHide() + public void ForceHide() // 함수를 선언할거에요 -> 강제 숨김 기능인 ForceHide를 { - SetRenderersState(false); - _isCurrentlyVisible = false; + SetRenderersState(false); // 함수를 실행할거에요 -> 모든 렌더러를 끄는 기능을 + _isCurrentlyVisible = false; // 상태를 설정할거에요 -> 숨김 중인 것으로 } /// /// 그룹 유효성 검사 (Renderer가 파괴되었는지 확인) /// - public bool IsValid() + public bool IsValid() // 함수를 선언할거에요 -> 유효성 검사 기능인 IsValid를 { - if (_renderers == null || _renderers.Length == 0) - return false; + if (_renderers == null || _renderers.Length == 0) // 조건이 맞으면 반환할거에요 -> 배열이 비어있다면 거짓(false)을 + return false; // 반환할거에요 -> 유효하지 않음을 // 하나라도 유효한 Renderer가 있으면 true - foreach (Renderer r in _renderers) + foreach (Renderer r in _renderers) // 반복할거에요 -> 배열 내 모든 렌더러에 대해 { - if (r != null) return true; + if (r != null) return true; // 조건이 맞으면 반환할거에요 -> 살아있는 객체가 있다면 참(true)을 } - return false; + return false; // 반환할거에요 -> 모두 파괴되었다면 거짓을 } #endregion @@ -143,73 +143,73 @@ namespace GameSystems.Optimization /// /// 여러 Renderer의 통합 중심점 계산 /// - private static Vector3 CalculateGroupCenter(Renderer[] renderers) + private static Vector3 CalculateGroupCenter(Renderer[] renderers) // 함수를 선언할거에요 -> 중심점을 계산하는 정적 기능을 { - if (renderers.Length == 1 && renderers[0] != null) + if (renderers.Length == 1 && renderers[0] != null) // 조건이 맞으면 실행할거에요 -> 렌더러가 하나뿐이라면 { - return renderers[0].bounds.center; + return renderers[0].bounds.center; // 반환할거에요 -> 그 렌더러의 중심점을 } // 모든 bounds를 포함하는 통합 경계 계산 - Bounds? combinedBounds = null; + Bounds? combinedBounds = null; // 변수를 선언할거에요 -> 널 허용 경계값 변수를 - foreach (Renderer r in renderers) + foreach (Renderer r in renderers) // 반복할거에요 -> 모든 렌더러에 대해 { - if (r == null) continue; + if (r == null) continue; // 조건이 맞으면 건너뛸거에요 -> 렌더러가 비어있다면 - if (!combinedBounds.HasValue) + if (!combinedBounds.HasValue) // 조건이 맞으면 실행할거에요 -> 첫 번째 유효한 경계값이라면 { - combinedBounds = r.bounds; + combinedBounds = r.bounds; // 값을 할당할거에요 -> 현재 렌더러의 경계값으로 } - else + else // 조건이 틀리면 실행할거에요 -> 이미 경계값이 존재한다면 { - Bounds temp = combinedBounds.Value; - temp.Encapsulate(r.bounds); - combinedBounds = temp; + Bounds temp = combinedBounds.Value; // 변수를 설정할거에요 -> 임시 경계값으로 + temp.Encapsulate(r.bounds); // 영역을 확장할거에요 -> 새로운 렌더러를 포함하도록 + combinedBounds = temp; // 값을 갱신할거에요 -> 확장된 경계값으로 } } - return combinedBounds?.center ?? Vector3.zero; + return combinedBounds?.center ?? Vector3.zero; // 반환할거에요 -> 계산된 중심점을 (없으면 제로 벡터) } /// /// 오브젝트의 최대 반지름 계산 /// - private static float CalculateBoundingRadius(Renderer[] renderers, Vector3 center) + private static float CalculateBoundingRadius(Renderer[] renderers, Vector3 center) // 함수를 선언할거에요 -> 반지름을 계산하는 정적 기능을 { - float maxRadius = 0f; + float maxRadius = 0f; // 변수를 초기화할거에요 -> 최대 반지름 수치를 0으로 - foreach (Renderer r in renderers) + foreach (Renderer r in renderers) // 반복할거에요 -> 모든 렌더러에 대해 { - if (r == null) continue; + if (r == null) continue; // 조건이 맞으면 건너뛸거에요 -> 렌더러가 비어있다면 // 각 Renderer 중심에서의 거리 + extent 크기 - float distanceFromCenter = Vector3.Distance(r.bounds.center, center); - float extent = r.bounds.extents.magnitude; - float totalRadius = distanceFromCenter + extent; + float distanceFromCenter = Vector3.Distance(r.bounds.center, center); // 거리를 계산할거에요 -> 렌더러 중심과 그룹 중심 사이의 + float extent = r.bounds.extents.magnitude; // 크기를 가져올거에요 -> 경계 상자의 대각선 길이를 + float totalRadius = distanceFromCenter + extent; // 값을 합산할거에요 -> 거리와 크기를 더해서 - if (totalRadius > maxRadius) + if (totalRadius > maxRadius) // 조건이 맞으면 실행할거에요 -> 현재 합계가 더 크다면 { - maxRadius = totalRadius; + maxRadius = totalRadius; // 값을 갱신할거에요 -> 새로운 최대 반지름으로 } } - return maxRadius; + return maxRadius; // 반환할거에요 -> 최종 계산된 최대 반지름을 } /// /// 모든 Renderer의 enabled 상태 일괄 변경 /// - private void SetRenderersState(bool enabled) + private void SetRenderersState(bool enabled) // 함수를 선언할거에요 -> 렌더러 상태를 일괄 변경하는 기능을 { - for (int i = 0; i < _renderers.Length; i++) + for (int i = 0; i < _renderers.Length; i++) // 반복할거에요 -> 배열의 인덱스만큼 { - Renderer r = _renderers[i]; - + Renderer r = _renderers[i]; // 객체를 가져올거에요 -> i번째 렌더러를 + // null 체크 + 상태가 다를 때만 변경 (성능 최적화) - if (r != null && r.enabled != enabled) + if (r != null && r.enabled != enabled) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재하고 상태 변경이 필요하다면 { - r.enabled = enabled; + r.enabled = enabled; // 설정을 바꿀거에요 -> 활성화 여부를 인자값대로 } } } @@ -221,12 +221,12 @@ namespace GameSystems.Optimization /// /// 리소스 정리 (명시적 호출용) /// - public void Dispose() + public void Dispose() // 함수를 선언할거에요 -> 자원 정리 기능을 { // Renderer 배열 초기화 (GC 도움) - System.Array.Clear(_renderers, 0, _renderers.Length); + System.Array.Clear(_renderers, 0, _renderers.Length); // 비울거에요 -> 렌더러 배열의 모든 요소를 } #endregion } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Optimization/RenderOptimizationConfig.cs b/Assets/Scripts/Systems/Optimization/RenderOptimizationConfig.cs index 1ccb2e59..d601a582 100644 --- a/Assets/Scripts/Systems/Optimization/RenderOptimizationConfig.cs +++ b/Assets/Scripts/Systems/Optimization/RenderOptimizationConfig.cs @@ -1,94 +1,81 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -namespace GameSystems.Optimization +namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로 { /// /// PlayerRangeManager의 설정을 관리하는 ScriptableObject - /// - 프로젝트 전역 설정 공유 - /// - 런타임 수정 불가 (데이터 무결성 보장) /// - [CreateAssetMenu(fileName = "RenderOptimizationConfig", menuName = "Game Systems/Render Optimization Config")] - public sealed class RenderOptimizationConfig : ScriptableObject + [CreateAssetMenu(fileName = "RenderOptimizationConfig", menuName = "Game Systems/Render Optimization Config")] // 에셋 생성 메뉴를 만들거에요 -> 지정된 경로에 + public sealed class RenderOptimizationConfig : ScriptableObject // 클래스를 선언할거에요 -> ScriptableObject를 상속받는 RenderOptimizationConfig를 { #region Inspector Fields - [Header("=== 렌더링 범위 설정 ===")] - [Tooltip("플레이어 주변 렌더링 반지름 (유니티 단위)")] - [SerializeField, Range(10f, 200f)] - private float _renderRange = 40f; + [Header("=== 렌더링 범위 설정 ===")] // 인스펙터에 제목을 표시할거에요 -> === 렌더링 범위 설정 === 을 + [Tooltip("플레이어 주변 렌더링 반지름 (유니티 단위)")] // 툴팁을 추가할거에요 -> 설명 문구를 + [SerializeField, Range(10f, 200f)] // 슬라이더 범위를 지정할거에요 -> 10에서 200 사이로 + private float _renderRange = 40f; // 변수를 선언할거에요 -> 기본 렌더링 범위인 40을 _renderRange에 - [Tooltip("페이드 아웃 시작 거리 비율 (0.8 = 렌더 범위의 80%부터 페이드 시작)")] - [SerializeField, Range(0.5f, 1f)] - private float _fadeStartRatio = 0.8f; + [Tooltip("페이드 아웃 시작 거리 비율 (0.8 = 렌더 범위의 80%부터 페이드 시작)")] // 툴팁을 추가할거에요 -> 비율 설명을 + [SerializeField, Range(0.5f, 1f)] // 슬라이더 범위를 지정할거에요 -> 0.5에서 1 사이로 + private float _fadeStartRatio = 0.8f; // 변수를 선언할거에요 -> 페이드 시작 비율인 0.8을 _fadeStartRatio에 - [Header("=== 업데이트 주기 ===")] - [Tooltip("오브젝트 컬링 검사 주기 (초 단위)")] - [SerializeField, Range(0.05f, 1f)] - private float _checkInterval = 0.2f; + [Header("=== 업데이트 주기 ===")] // 제목을 표시할거에요 -> === 업데이트 주기 === 를 + [Tooltip("오브젝트 컬링 검사 주기 (초 단위)")] // 툴팁을 추가할거에요 -> 검사 주기 설명을 + [SerializeField, Range(0.05f, 1f)] // 슬라이더 범위를 지정할거에요 -> 0.05에서 1 사이로 + private float _checkInterval = 0.2f; // 변수를 선언할거에요 -> 검사 간격인 0.2를 _checkInterval에 - [Header("=== 플레이어 설정 ===")] - [Tooltip("플레이어를 찾을 태그 이름")] - [SerializeField] - private string _playerTag = "Player"; + [Header("=== 플레이어 설정 ===")] // 제목을 표시할거에요 -> === 플레이어 설정 === 을 + [Tooltip("플레이어를 찾을 태그 이름")] // 툴팁을 추가할거에요 -> 태그 이름 설명을 + [SerializeField] // 필드를 직렬화할거에요 -> 인스펙터에 보이게 + private string _playerTag = "Player"; // 변수를 선언할거에요 -> 플레이어 태그인 "Player"를 _playerTag에 - [Header("=== 성능 설정 ===")] - [Tooltip("한 프레임에 처리할 최대 오브젝트 수 (0 = 무제한)")] - [SerializeField, Range(0, 500)] - private int _maxObjectsPerFrame = 0; + [Header("=== 성능 설정 ===")] // 제목을 표시할거에요 -> === 성능 설정 === 을 + [Tooltip("한 프레임에 처리할 최대 오브젝트 수 (0 = 무제한)")] // 툴팁을 추가할거에요 -> 처리량 설명을 + [SerializeField, Min(0)] // 최소값을 지정할거에요 -> 0으로 + private int _maxObjectsPerFrame = 50; // 변수를 선언할거에요 -> 프레임당 최대 개수인 50을 _maxObjectsPerFrame에 - [Tooltip("비활성 오브젝트도 스캔할지 여부")] - [SerializeField] - private bool _includeInactiveObjects = true; - - [Header("=== 디버그 설정 ===")] - [Tooltip("기즈모 표시 여부")] - [SerializeField] - private bool _showGizmos = true; - - [Tooltip("상세 로그 출력")] - [SerializeField] - private bool _enableVerboseLogging = false; + [Header("=== 디버그 설정 ===")] // 제목을 표시할거에요 -> === 디버그 설정 === 을 + [SerializeField] private bool _showGizmos = true; // 변수를 선언할거에요 -> 기즈모 표시 여부를 _showGizmos에 + [SerializeField] private bool _enableVerboseLogging = false; // 변수를 선언할거에요 -> 상세 로그 여부를 _enableVerboseLogging에 #endregion - #region Public Properties (Immutable) + #region Public Properties - public float RenderRange => _renderRange; - public float FadeStartRatio => _fadeStartRatio; - public float CheckInterval => _checkInterval; - public string PlayerTag => _playerTag; - public int MaxObjectsPerFrame => _maxObjectsPerFrame; - public bool IncludeInactiveObjects => _includeInactiveObjects; - public bool ShowGizmos => _showGizmos; - public bool EnableVerboseLogging => _enableVerboseLogging; + public float RenderRange => _renderRange; // 값을 반환할거에요 -> 렌더링 범위 수치를 + public float CheckInterval => _checkInterval; // 값을 반환할거에요 -> 검사 주기 수치를 + public string PlayerTag => _playerTag; // 값을 반환할거에요 -> 플레이어 태그 문자열을 + public int MaxObjectsPerFrame => _maxObjectsPerFrame; // 값을 반환할거에요 -> 최대 처리 개수를 + public bool ShowGizmos => _showGizmos; // 값을 반환할거에요 -> 기즈모 활성화 여부를 + public bool EnableVerboseLogging => _enableVerboseLogging; // 값을 반환할거에요 -> 로그 활성화 여부를 /// /// 페이드 시작 거리 계산 /// - public float FadeStartDistance => _renderRange * _fadeStartRatio; + public float FadeStartDistance => _renderRange * _fadeStartRatio; // 값을 계산해서 반환할거에요 -> 렌더링 범위에 비율을 곱한 결과를 #endregion #region Validation - private void OnValidate() + private void OnValidate() // 값이 변경될 때 실행할거에요 -> OnValidate 함수를 { // 유효성 검사 - if (_renderRange < 10f) + if (_renderRange < 10f) // 조건이 맞으면 실행할거에요 -> 렌더링 범위가 10보다 작다면 { - Debug.LogWarning("[RenderOptimizationConfig] Render Range가 너무 작습니다. 최소 10으로 설정됩니다."); - _renderRange = 10f; + Debug.LogWarning("[RenderOptimizationConfig] Render Range가 너무 작습니다. 최소 10으로 설정됩니다."); // 경고를 출력할거에요 -> 범위 오류 메시지를 + _renderRange = 10f; // 값을 설정할거에요 -> 10으로 } - if (string.IsNullOrWhiteSpace(_playerTag)) + if (string.IsNullOrWhiteSpace(_playerTag)) // 조건이 맞으면 실행할거에요 -> 플레이어 태그가 비어있다면 { - Debug.LogWarning("[RenderOptimizationConfig] Player Tag가 비어있습니다. 'Player'로 설정됩니다."); - _playerTag = "Player"; + Debug.LogWarning("[RenderOptimizationConfig] Player Tag가 비어있습니다. 'Player'로 설정됩니다."); // 경고를 출력할거에요 -> 태그 오류 메시지를 + _playerTag = "Player"; // 값을 설정할거에요 -> "Player"로 } - if (_checkInterval < 0.05f) + if (_checkInterval < 0.05f) // 조건이 맞으면 실행할거에요 -> 검사 간격이 0.05보다 작다면 { - Debug.LogWarning("[RenderOptimizationConfig] Check Interval이 너무 짧습니다. 성능에 영향을 줄 수 있습니다."); + Debug.LogWarning("[RenderOptimizationConfig] Check Interval이 너무 짧습니다. 성능에 영향을 줄 수 있습니다."); // 경고를 출력할거에요 -> 성능 영향 메시지를 } } @@ -99,16 +86,16 @@ namespace GameSystems.Optimization /// /// 기본 설정 생성 /// - public static RenderOptimizationConfig CreateDefault() + public static RenderOptimizationConfig CreateDefault() // 함수를 선언할거에요 -> 기본 설정을 생성하는 CreateDefault를 { - RenderOptimizationConfig config = CreateInstance(); - config._renderRange = 40f; - config._checkInterval = 0.2f; - config._playerTag = "Player"; - config._showGizmos = true; - return config; + RenderOptimizationConfig config = CreateInstance(); // 인스턴스를 생성할거에요 -> RenderOptimizationConfig 객체를 + config._renderRange = 40f; // 값을 설정할거에요 -> 40으로 + config._checkInterval = 0.2f; // 값을 설정할거에요 -> 0.2로 + config._playerTag = "Player"; // 값을 설정할거에요 -> "Player"로 + config._showGizmos = true; // 값을 설정할거에요 -> 참(true)으로 + return config; // 반환할거에요 -> 생성된 설정 객체를 } #endregion } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Scene/LoadScene.cs b/Assets/Scripts/Systems/Scene/LoadScene.cs index 84413b48..4cb69baf 100644 --- a/Assets/Scripts/Systems/Scene/LoadScene.cs +++ b/Assets/Scripts/Systems/Scene/LoadScene.cs @@ -1,14 +1,14 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.SceneManagement; +using System.Collections; // ÷ Ұſ -> System.Collections +using System.Collections.Generic; // ׸ ÷ Ұſ -> System.Collections.Generic +using UnityEngine; // Ƽ ⺻ ҷðſ -> UnityEngine +using UnityEngine.SceneManagement; // Ұſ -> UnityEngine.SceneManagement -public class LoadScene : MonoBehaviour +public class LoadScene : MonoBehaviour // Ŭ Ұſ -> MonoBehaviour ӹ޴ LoadScene { - [SerializeField] private string sceneName; - - public void LoadOtherScene() + [SerializeField] private string sceneName; // Ұſ -> ε ̸ sceneName + + public void LoadOtherScene() // Լ Ұſ -> ٸ εϴ LoadOtherScene { - SceneManager.LoadScene(sceneName); + SceneManager.LoadScene(sceneName); // Ұſ -> ̸ εϴ } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Settings/GameStopPanelController.cs b/Assets/Scripts/Systems/Settings/GameStopPanelController.cs index ff409fb1..667b9ade 100644 --- a/Assets/Scripts/Systems/Settings/GameStopPanelController.cs +++ b/Assets/Scripts/Systems/Settings/GameStopPanelController.cs @@ -1,22 +1,52 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; +using UnityEngine; // Ƽ ⺻ ҷðſ -> UnityEngine +using UnityEngine.SceneManagement; // Ұſ -> UnityEngine.SceneManagement -public class GameStopPanelController : MonoBehaviour +public class GameStopPanelController : MonoBehaviour // Ŭ Ұſ -> MonoBehaviour ӹ޴ GameStopPanelController { - [SerializeField] private GameObject gameStopPanel; - [SerializeField] private SettingPanelController settingPanelController; - private bool isGameStopping = false; + [Header("UI ")] // ν â ǥҰſ -> UI + [SerializeField] private GameObject stopPanel; // Ұſ -> Ͻ г Ʈ stopPanel - private void Update() + private bool isPaused = false; // ʱȭҰſ -> Ͻ θ + + private void Update() // Լ Ұſ -> Ӹ Update { - if (settingPanelController != null && settingPanelController.IsSettingOpen) - return; - - if (Input.GetKeyDown(KeyCode.Escape)) + // ESC Ű Է + if (Input.GetKeyDown(KeyCode.Escape)) // Ұſ -> ESC Ű ٸ { - isGameStopping = !isGameStopping; - gameStopPanel.SetActive(isGameStopping); + TogglePause(); // Լ Ұſ -> Ͻ ¸ ϴ TogglePause } } -} + + public void TogglePause() // Լ Ұſ -> Ͻ Ѱ TogglePause + { + isPaused = !isPaused; // ¸ ſ -> Ͻ θ ݴ + + if (stopPanel != null) stopPanel.SetActive(isPaused); // ٲܰſ -> г Ȱȭ ¸ isPaused + + if (isPaused) // Ұſ -> Ͻ ¶ + { + Time.timeScale = 0f; // Ұſ -> ð + } + else // Ʋ Ұſ -> ¶ + { + Time.timeScale = 1f; // Ұſ -> ð ӵ + } + } + + // " ޴" ư Լ + public void GoToMainMenu() // Լ Ұſ -> ޴ ̵ϴ GoToMainMenu + { + Time.timeScale = 1f; // Ұſ -> ð ( ̵ ʼ) + SceneManager.LoadScene("MainMenu"); // Ұſ -> "MainMenu" εϴ + } + + // " " ư Լ + public void QuitGame() // Լ Ұſ -> ϴ QuitGame + { +#if UNITY_EDITOR // Ǻ Ұſ -> Ƽ ȯ̶ + UnityEditor.EditorApplication.isPlaying = false; // ٲܰſ -> ÷ 带 +#else // ȯ()̶ + Application.Quit(); // Ұſ -> ø̼ Ḧ +#endif // Ǻ ſ + } +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Settings/GraphicSettingUI.cs b/Assets/Scripts/Systems/Settings/GraphicSettingUI.cs index 4231265a..d616b0a0 100644 --- a/Assets/Scripts/Systems/Settings/GraphicSettingUI.cs +++ b/Assets/Scripts/Systems/Settings/GraphicSettingUI.cs @@ -1,19 +1,50 @@ -using System.Collections; -using System.Collections.Generic; -using TMPro; -using UnityEngine; +using UnityEngine; // Ƽ ⺻ ҷðſ -> UnityEngine +using UnityEngine.UI; // Ƽ UI Ұſ -> UnityEngine.UI +using TMPro; // ؽƮ޽ Ұſ -> TMPro -public class GraphicSettingUI : MonoBehaviour +public class GraphicSettingUI : MonoBehaviour // Ŭ Ұſ -> MonoBehaviour ӹ޴ GraphicSettingUI { - [SerializeField] private TMP_Dropdown qualityDropdown; + [Header("ü ȭ ")] // ν â ǥҰſ -> ü ȭ + [SerializeField] private Toggle fullScreenToggle; // Ұſ -> ü ȭ UI fullScreenToggle - private void OnEnable() + [Header("ȭ (Ӵٿ)")] // ν â ǥҰſ -> ȭ (Ӵٿ) + [SerializeField] private TMP_Dropdown qualityDropdown; // Ұſ -> ȭ Ӵٿ UI qualityDropdown + + private void Start() // Լ Ұſ -> ũƮ Start { - qualityDropdown.value = SettingsManager.Instance.qualityIndex; + // ҷͼ UI ݿ + LoadCurrentSettings(); // Լ Ұſ -> UI ǥϴ LoadCurrentSettings + + // ̺Ʈ + if (fullScreenToggle != null) // Ұſ -> ü ȭ ִٸ + fullScreenToggle.onValueChanged.AddListener(OnFullScreenChanged); // Ұſ -> OnFullScreenChanged ȣ + + if (qualityDropdown != null) // Ұſ -> ȭ Ӵٿ ִٸ + qualityDropdown.onValueChanged.AddListener(OnQualityChanged); // Ұſ -> OnQualityChanged ȣ } - public void OnQualityChanged(int index) + private void LoadCurrentSettings() // Լ Ұſ -> UI ݿϴ LoadCurrentSettings { - SettingsManager.Instance.SetQuality(index); + if (SettingsManager.Instance == null) return; // ߴҰſ -> Ŵ ٸ + + // ü ȭ ȭ + if (fullScreenToggle != null) // Ұſ -> ִٸ + fullScreenToggle.isOn = SettingsManager.Instance.isFullScreen; // Ұſ -> Ŵ ü ȭ + + // ȭ Ӵٿ ȭ + if (qualityDropdown != null) // Ұſ -> Ӵٿ ִٸ + qualityDropdown.value = SettingsManager.Instance.qualityIndex; // Ұſ -> Ŵ ȭ ε } -} + + public void OnFullScreenChanged(bool isFull) // Լ Ұſ -> ü ȭ ȣ OnFullScreenChanged + { + if (SettingsManager.Instance != null) // Ұſ -> Ŵ ִٸ + SettingsManager.Instance.SetFullScreen(isFull); // Ұſ -> ü ȭ ϴ Լ + } + + public void OnQualityChanged(int index) // Լ Ұſ -> ȭ ȣ OnQualityChanged + { + if (SettingsManager.Instance != null) // Ұſ -> Ŵ ִٸ + SettingsManager.Instance.SetQuality(index); // Ұſ -> ȭ ϴ Լ + } +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Settings/SettingResolutionUI.cs b/Assets/Scripts/Systems/Settings/SettingResolutionUI.cs index 621590f8..380981ce 100644 --- a/Assets/Scripts/Systems/Settings/SettingResolutionUI.cs +++ b/Assets/Scripts/Systems/Settings/SettingResolutionUI.cs @@ -1,37 +1,50 @@ -using System.Collections; -using System.Collections.Generic; -using TMPro; -using UnityEngine; -using UnityEngine.UI; +using UnityEngine; // Ƽ ⺻ ҷðſ -> UnityEngine +using TMPro; // ؽƮ޽ Ұſ -> TMPro +using System.Collections.Generic; // Ʈ Ұſ -> System.Collections.Generic -public class SettingResolutionUI : MonoBehaviour +public class SettingResolutionUI : MonoBehaviour // Ŭ Ұſ -> MonoBehaviour ӹ޴ SettingResolutionUI { - [SerializeField] private TMP_Dropdown resDropDown; - [SerializeField] private Toggle fullScreenToggle; + [SerializeField] private TMP_Dropdown resolutionDropdown; // Ұſ -> ػ Ӵٿ UI resolutionDropdown - private void Start() + private Resolution[] resolutions; // 迭 Ұſ -> ػ resolutions + + private void Start() // Լ Ұſ -> ũƮ Start { - var manager = SettingsManager.Instance; - - resDropDown.ClearOptions(); - - var options = new List(); - foreach (var res in manager.FilteredResolutions) - options.Add($"{res.width} x {res.height}"); - - resDropDown.AddOptions(options); - - resDropDown.value = manager.resolutionIndex; - fullScreenToggle.isOn = manager.isFullScreen; + InitDropdown(); // Լ Ұſ -> Ӵٿ ʱȭϴ InitDropdown } - public void OnResolutionChanged(int index) + private void InitDropdown() // Լ Ұſ -> ػ ä InitDropdown { - SettingsManager.Instance.SetResolution(index); + if (SettingsManager.Instance == null || resolutionDropdown == null) return; // ߴҰſ -> Ŵ Ӵٿ ٸ + + resolutions = SettingsManager.Instance.FilteredResolutions; // ðſ -> Ŵ ͸ ػ + resolutionDropdown.ClearOptions(); // ſ -> Ӵٿ ɼǵ + + List options = new List(); // Ʈ Ұſ -> ɼ ڿ Ʈ + int currentResolutionIndex = 0; // ʱȭҰſ -> ػ ε 0 + + for (int i = 0; i < resolutions.Length; i++) // ݺҰſ -> ػ󵵿 + { + string option = resolutions[i].width + " x " + resolutions[i].height; // ڿ ſ -> " x " + options.Add(option); // ߰Ұſ -> ڿ ɼ Ʈ + + // ػ󵵿 ġϴ Ȯ + if (i == SettingsManager.Instance.resolutionIndex) // Ұſ -> ε ٸ + { + currentResolutionIndex = i; // Ұſ -> ε + } + } + + resolutionDropdown.AddOptions(options); // ߰Ұſ -> ϼ ɼ Ʈ Ӵٿ + resolutionDropdown.value = currentResolutionIndex; // Ұſ -> õ ػ󵵷 + resolutionDropdown.RefreshShownValue(); // Ұſ -> Ӵٿ ǥ + + resolutionDropdown.onValueChanged.AddListener(OnResolutionChanged); // Ұſ -> ̺Ʈ } - public void OnFullScreenChanged(bool value) + public void OnResolutionChanged(int index) // Լ Ұſ -> ػ ȣ OnResolutionChanged { - SettingsManager.Instance.SetFullScreen(value); + if (SettingsManager.Instance != null) // Ұſ -> Ŵ ִٸ + SettingsManager.Instance.SetResolution(index); // Ұſ -> ػ ϴ Լ } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Settings/SettingsManager.cs b/Assets/Scripts/Systems/Settings/SettingsManager.cs index 8bddfe4e..19fdcc43 100644 --- a/Assets/Scripts/Systems/Settings/SettingsManager.cs +++ b/Assets/Scripts/Systems/Settings/SettingsManager.cs @@ -1,20 +1,20 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.SceneManagement; +using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.SceneManagement; // 씬 관리 기능을 사용할거에요 -> UnityEngine.SceneManagement를 -public class SettingsManager : MonoBehaviour +public class SettingsManager : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 SettingsManager를 { - public static SettingsManager Instance; + public static SettingsManager Instance; // 변수를 선언할거에요 -> 싱글톤 인스턴스인 Instance를 - public int qualityIndex; - public int resolutionIndex; - public bool isFullScreen; + public int qualityIndex; // 변수를 선언할거에요 -> 그래픽 품질 인덱스를 qualityIndex에 + public int resolutionIndex; // 변수를 선언할거에요 -> 해상도 인덱스를 resolutionIndex에 + public bool isFullScreen; // 변수를 선언할거에요 -> 전체 화면 여부를 isFullScreen에 - private Resolution[] allResolutions; - public Resolution[] FilteredResolutions { get; private set; } + private Resolution[] allResolutions; // 배열을 선언할거에요 -> 지원하는 모든 해상도를 담을 allResolutions를 + public Resolution[] FilteredResolutions { get; private set; } // 프로퍼티를 선언할거에요 -> 필터링된 해상도 목록을 외부에서 읽기만 가능하게 - private readonly Vector2Int[] commonResolutions = + private readonly Vector2Int[] commonResolutions = // 배열을 초기화할거에요 -> 공통 해상도 목록을 읽기 전용으로 { new(1280, 720), new(1280, 768), @@ -25,127 +25,127 @@ public class SettingsManager : MonoBehaviour new(1920, 1080) }; - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를 { - Debug.Log("SettingsManager Awake: " + gameObject.name); + Debug.Log("SettingsManager Awake: " + gameObject.name); // 로그를 출력할거에요 -> 매니저 시작 알림을 - if (Instance != null) + if (Instance != null) // 조건이 맞으면 실행할거에요 -> 이미 인스턴스가 존재한다면 { - Destroy(gameObject); - return; + Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를 + return; // 중단할거에요 -> 초기화 로직을 } - Instance = this; - DontDestroyOnLoad(gameObject); + Instance = this; // 값을 저장할거에요 -> 나 자신을 싱글톤 인스턴스에 + DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬이 바뀌어도 파괴되지 않게 - InitResolutions(); - LoadSettings(); + InitResolutions(); // 함수를 실행할거에요 -> 해상도 목록을 초기화하는 InitResolutions를 + LoadSettings(); // 함수를 실행할거에요 -> 저장된 설정을 불러오는 LoadSettings를 // 🔥 추가 - SyncResolutionIndexWithCurrentScreen(); + SyncResolutionIndexWithCurrentScreen(); // 함수를 실행할거에요 -> 현재 화면 해상도와 인덱스를 동기화하는 함수를 - ApplySettings(); + ApplySettings(); // 함수를 실행할거에요 -> 설정을 실제로 적용하는 ApplySettings를 } - void InitResolutions() + void InitResolutions() // 함수를 선언할거에요 -> 해상도 목록을 초기화하는 InitResolutions를 { - allResolutions = Screen.resolutions; + allResolutions = Screen.resolutions; // 값을 가져올거에요 -> 모니터가 지원하는 모든 해상도를 - var list = new System.Collections.Generic.List(); + var list = new System.Collections.Generic.List(); // 리스트를 생성할거에요 -> 해상도를 담을 임시 리스트를 - foreach (var res in allResolutions) + foreach (var res in allResolutions) // 반복할거에요 -> 모든 해상도에 대해 { - foreach (var common in commonResolutions) + foreach (var common in commonResolutions) // 반복할거에요 -> 공통 해상도 목록과 비교하기 위해 { - if (res.width == common.x && res.height == common.y) + if (res.width == common.x && res.height == common.y) // 조건이 맞으면 실행할거에요 -> 가로세로가 공통 해상도와 일치한다면 { - if (!list.Exists(r => r.width == res.width && r.height == res.height)) - list.Add(res); + if (!list.Exists(r => r.width == res.width && r.height == res.height)) // 조건이 맞으면 실행할거에요 -> 리스트에 없는 해상도라면 (중복 방지) + list.Add(res); // 추가할거에요 -> 리스트에 해당 해상도를 } } } - FilteredResolutions = list.ToArray(); + FilteredResolutions = list.ToArray(); // 변환할거에요 -> 리스트를 배열로 바꿔서 FilteredResolutions에 } // ===== 적용 메서드 ===== - public void SetQuality(int index) + public void SetQuality(int index) // 함수를 선언할거에요 -> 그래픽 품질을 설정하는 SetQuality를 { - qualityIndex = index; - QualitySettings.SetQualityLevel(index); - PlayerPrefs.SetInt("Quality", index); + qualityIndex = index; // 값을 저장할거에요 -> 품질 인덱스를 + QualitySettings.SetQualityLevel(index); // 설정을 바꿀거에요 -> 유니티 품질 설정을 해당 레벨로 + PlayerPrefs.SetInt("Quality", index); // 저장할거에요 -> 품질 인덱스를 로컬 저장소에 } - public void SetResolution(int index) + public void SetResolution(int index) // 함수를 선언할거에요 -> 해상도를 설정하는 SetResolution을 { - resolutionIndex = index; - ApplyResolution(); - PlayerPrefs.SetInt("Resolution", index); + resolutionIndex = index; // 값을 저장할거에요 -> 해상도 인덱스를 + ApplyResolution(); // 함수를 실행할거에요 -> 해상도를 적용하는 ApplyResolution을 + PlayerPrefs.SetInt("Resolution", index); // 저장할거에요 -> 해상도 인덱스를 로컬 저장소에 } - public void SetFullScreen(bool full) + public void SetFullScreen(bool full) // 함수를 선언할거에요 -> 전체 화면을 설정하는 SetFullScreen을 { - isFullScreen = full; + isFullScreen = full; // 값을 저장할거에요 -> 전체 화면 여부를 - Screen.fullScreenMode = full - ? FullScreenMode.FullScreenWindow - : FullScreenMode.Windowed; + Screen.fullScreenMode = full // 설정을 바꿀거에요 -> 화면 모드를 + ? FullScreenMode.FullScreenWindow // 조건이 참이면 전체 화면 창 모드로 + : FullScreenMode.Windowed; // 조건이 거짓이면 창 모드로 - ApplyResolution(); - PlayerPrefs.SetInt("FullScreen", full ? 1 : 0); + ApplyResolution(); // 함수를 실행할거에요 -> 해상도 적용 함수를 + PlayerPrefs.SetInt("FullScreen", full ? 1 : 0); // 저장할거에요 -> 전체 화면 여부를 (1:참, 0:거짓) } - void ApplyResolution() + void ApplyResolution() // 함수를 선언할거에요 -> 해상도를 실제 적용하는 ApplyResolution을 { - var res = FilteredResolutions[resolutionIndex]; - Screen.SetResolution( - res.width, - res.height, - isFullScreen ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed + var res = FilteredResolutions[resolutionIndex]; // 값을 가져올거에요 -> 선택된 해상도 정보를 + Screen.SetResolution( // 설정을 바꿀거에요 -> 화면 해상도를 + res.width, // 가로 크기로 + res.height, // 세로 크기로 + isFullScreen ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed // 화면 모드에 맞춰서 ); } - void LoadSettings() + void LoadSettings() // 함수를 선언할거에요 -> 저장된 설정을 불러오는 LoadSettings를 { - qualityIndex = PlayerPrefs.GetInt("Quality", QualitySettings.names.Length - 1); - resolutionIndex = PlayerPrefs.GetInt("Resolution", 0); - isFullScreen = PlayerPrefs.GetInt("FullScreen", 1) == 1; + qualityIndex = PlayerPrefs.GetInt("Quality", QualitySettings.names.Length - 1); // 불러올거에요 -> 품질 인덱스를 (없으면 최대값) + resolutionIndex = PlayerPrefs.GetInt("Resolution", 0); // 불러올거에요 -> 해상도 인덱스를 (없으면 0) + isFullScreen = PlayerPrefs.GetInt("FullScreen", 1) == 1; // 불러올거에요 -> 전체 화면 여부를 (없으면 1=참) } - void ApplySettings() + void ApplySettings() // 함수를 선언할거에요 -> 모든 설정을 적용하는 ApplySettings를 { - SetQuality(qualityIndex); - Screen.fullScreenMode = isFullScreen - ? FullScreenMode.FullScreenWindow - : FullScreenMode.Windowed; - ApplyResolution(); + SetQuality(qualityIndex); // 실행할거에요 -> 품질 설정을 + Screen.fullScreenMode = isFullScreen // 설정을 바꿀거에요 -> 화면 모드를 + ? FullScreenMode.FullScreenWindow // 조건이 참이면 전체 화면으로 + : FullScreenMode.Windowed; // 조건이 거짓이면 창 모드로 + ApplyResolution(); // 실행할거에요 -> 해상도 적용을 } - void SyncResolutionIndexWithCurrentScreen() + void SyncResolutionIndexWithCurrentScreen() // 함수를 선언할거에요 -> 현재 화면과 해상도 인덱스를 맞추는 SyncResolutionIndexWithCurrentScreen을 { - for (int i = 0; i < FilteredResolutions.Length; i++) + for (int i = 0; i < FilteredResolutions.Length; i++) // 반복할거에요 -> 필터링된 모든 해상도에 대해 { - if (FilteredResolutions[i].width == Screen.width && - FilteredResolutions[i].height == Screen.height) + if (FilteredResolutions[i].width == Screen.width && // 조건이 맞으면 실행할거에요 -> 너비가 현재 화면과 같고 + FilteredResolutions[i].height == Screen.height) // 높이가 현재 화면과 같다면 { - resolutionIndex = i; - return; + resolutionIndex = i; // 값을 저장할거에요 -> 해당 인덱스를 + return; // 중단할거에요 -> 함수를 (찾았으니까) } } } - private void OnEnable() + private void OnEnable() // 함수를 실행할거에요 -> 오브젝트 활성화 시 OnEnable을 { - SceneManager.sceneLoaded += OnSceneLoaded; + SceneManager.sceneLoaded += OnSceneLoaded; // 구독할거에요 -> 씬 로드 이벤트를 } - private void OnDisable() + private void OnDisable() // 함수를 실행할거에요 -> 오브젝트 비활성화 시 OnDisable을 { - SceneManager.sceneLoaded -= OnSceneLoaded; + SceneManager.sceneLoaded -= OnSceneLoaded; // 구독 해제할거에요 -> 씬 로드 이벤트를 } - private void OnSceneLoaded(Scene scene, LoadSceneMode mode) + private void OnSceneLoaded(Scene scene, LoadSceneMode mode) // 함수를 선언할거에요 -> 씬 로드 완료 시 호출될 OnSceneLoaded를 { - ApplySettings(); // 🔥 씬 로드 직후 다시 적용 + ApplySettings(); // 🔥 씬 로드 직후 다시 적용 // 실행할거에요 -> 설정 적용 함수를 } -} +} \ No newline at end of file diff --git a/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs b/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs index 4272a4d4..12adda1d 100644 --- a/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs +++ b/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs @@ -1,42 +1,42 @@ -using UnityEngine; -using System; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System; // 기본 시스템 기능을 사용할거에요 -> System을 -public class EnemyHealth : MonoBehaviour, IDamageable +public class EnemyHealth : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 EnemyHealth를 { - [Header("--- 능력치 ---")] - [SerializeField] private float maxHealth = 50f; - private float _currentHealth; + [Header("--- 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 능력치 --- 를 + [SerializeField] private float maxHealth = 50f; // 변수를 선언할거에요 -> 최대 체력(50.0)을 maxHealth에 + private float _currentHealth; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHealth를 - public event Action OnHealthChanged; - public bool IsDead { get; private set; } + public event Action OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 시 현재/최대를 알릴 OnHealthChanged를 + public bool IsDead { get; private set; } // 프로퍼티를 선언할거에요 -> 사망 여부를 외부에서 읽기만 가능하게 IsDead에 - private void Start() + private void Start() // 함수를 실행할거에요 -> 시작 시 Start를 { - _currentHealth = maxHealth; + _currentHealth = maxHealth; // 값을 초기화할거에요 -> 현재 체력을 최대 체력으로 // ⭐ 시작하자마자 UI에 현재 숫자가 뜨도록 신호 발송 - OnHealthChanged?.Invoke(_currentHealth, maxHealth); + OnHealthChanged?.Invoke(_currentHealth, maxHealth); // 이벤트를 실행할거에요 -> 초기 체력 상태를 알리기 위해 } // IDamageable 인터페이스 구현 - public void TakeDamage(float amount) + public void TakeDamage(float amount) // 함수를 선언할거에요 -> 데미지를 입는 TakeDamage를 { - if (IsDead) return; + if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면 - _currentHealth = Mathf.Max(0, _currentHealth - amount); + _currentHealth = Mathf.Max(0, _currentHealth - amount); // 값을 계산할거에요 -> 체력에서 데미지를 빼고 0 밑으로 안 내려가게 // ⭐ 피격 시 로그와 함께 UI 갱신 신호 발송 - Debug.Log($"{gameObject.name} 피격! 체력: {{{_currentHealth}/{maxHealth}}}"); - OnHealthChanged?.Invoke(_currentHealth, maxHealth); + Debug.Log($"{gameObject.name} 피격! 체력: {{{_currentHealth}/{maxHealth}}}"); // 로그를 출력할거에요 -> 피격 정보와 남은 체력을 + OnHealthChanged?.Invoke(_currentHealth, maxHealth); // 이벤트를 실행할거에요 -> 변경된 체력 정보를 알리기 위해 - if (_currentHealth <= 0) Die(); + if (_currentHealth <= 0) Die(); // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 처리 Die를 } - private void Die() + private void Die() // 함수를 선언할거에요 -> 사망 처리를 하는 Die를 { - if (IsDead) return; - IsDead = true; + if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽음 처리 중이라면 + IsDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참으로 - Debug.Log($"{gameObject.name} 사망!"); - Destroy(gameObject, 1.5f); + Debug.Log($"{gameObject.name} 사망!"); // 로그를 출력할거에요 -> 사망 메시지를 + Destroy(gameObject, 1.5f); // 파괴할거에요 -> 이 오브젝트를 1.5초 뒤에 } } \ No newline at end of file diff --git a/Assets/Scripts/UI/HUD/CrossHairUI.cs b/Assets/Scripts/UI/HUD/CrossHairUI.cs index bcabf362..61806867 100644 --- a/Assets/Scripts/UI/HUD/CrossHairUI.cs +++ b/Assets/Scripts/UI/HUD/CrossHairUI.cs @@ -1,101 +1,100 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] -public class ArrowRangeUI : MonoBehaviour +[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] // 컴포넌트를 강제로 추가할거에요 -> MeshFilter와 MeshRenderer를 +public class ArrowRangeUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 ArrowRangeUI를 { - // 🚨 수정됨: 끝부분의 \ 기호를 모두 삭제했습니다. - [Header("--- 참조 ---")] - [SerializeField] private PlayerAttack attackScript; // PlayerAttack 스크립트 연결 + [Header("--- 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 참조 --- 를 + [SerializeField] private PlayerAttack attackScript; // 변수를 선언할거에요 -> 플레이어 공격 스크립트를 연결할 attackScript를 - [Header("--- UI 설정 ---")] - [Tooltip("사거리 표시기의 색상")] - [SerializeField] private Color uiColor = new Color(1f, 1f, 0f, 0.5f); // 노란색 반투명 - [Tooltip("화살표 UI의 폭 (두께)")] - [SerializeField] private float lineWidth = 1.0f; + [Header("--- UI 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- UI 설정 --- 을 + [Tooltip("사거리 표시기의 색상")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private Color uiColor = new Color(1f, 1f, 0f, 0.5f); // 변수를 선언할거에요 -> UI 색상(노란색 반투명)을 uiColor에 + [Tooltip("화살표 UI의 폭 (두께)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float lineWidth = 1.0f; // 변수를 선언할거에요 -> 선 두께(1.0)를 lineWidth에 - [Header("--- 길이 설정 ---")] - [Tooltip("차징 0%일 때의 최소 길이 (기본 사거리)")] - [SerializeField] private float minLength = 5f; - [Tooltip("풀차징일 때의 최대 길이 (최대 사거리)")] - [SerializeField] private float maxLength = 20f; + [Header("--- 길이 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 길이 설정 --- 을 + [Tooltip("차징 0%일 때의 최소 길이 (기본 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float minLength = 5f; // 변수를 선언할거에요 -> 최소 길이(5.0)를 minLength에 + [Tooltip("풀차징일 때의 최대 길이 (최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float maxLength = 20f; // 변수를 선언할거에요 -> 최대 길이(20.0)를 maxLength에 - private Mesh _mesh; - private MeshFilter _meshFilter; - private MeshRenderer _meshRenderer; + private Mesh _mesh; // 변수를 선언할거에요 -> UI를 그릴 메쉬를 _mesh에 + private MeshFilter _meshFilter; // 변수를 선언할거에요 -> 메쉬 필터 컴포넌트를 _meshFilter에 + private MeshRenderer _meshRenderer; // 변수를 선언할거에요 -> 메쉬 렌더러 컴포넌트를 _meshRenderer에 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를 { // 메쉬 초기화 - _mesh = new Mesh(); - _meshFilter = GetComponent(); - _meshRenderer = GetComponent(); - _meshFilter.mesh = _mesh; + _mesh = new Mesh(); // 생성할거에요 -> 빈 메쉬 객체를 + _meshFilter = GetComponent(); // 컴포넌트를 가져올거에요 -> 메쉬 필터를 + _meshRenderer = GetComponent(); // 컴포넌트를 가져올거에요 -> 메쉬 렌더러를 + _meshFilter.mesh = _mesh; // 할당할거에요 -> 생성한 메쉬를 필터에 // 재질(Material)이 없다면 임시로 생성 - if (_meshRenderer.material == null) + if (_meshRenderer.material == null) // 조건이 맞으면 실행할거에요 -> 렌더러에 재질이 없다면 { - _meshRenderer.material = new Material(Shader.Find("Sprites/Default")); + _meshRenderer.material = new Material(Shader.Find("Sprites/Default")); // 생성할거에요 -> 기본 스프라이트 쉐이더로 새 재질을 } - _meshRenderer.material.color = uiColor; + _meshRenderer.material.color = uiColor; // 색상을 설정할거에요 -> 지정한 UI 색상으로 } - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를 { // 1. 공격 스크립트가 없거나 차징 중이 아니면 끄기 - if (attackScript == null || !attackScript.IsCharging) + if (attackScript == null || !attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 없거나 차징 중이 아니라면 { - _meshRenderer.enabled = false; - return; + _meshRenderer.enabled = false; // 기능을 끌거에요 -> 렌더러를 (안 보이게) + return; // 중단할거에요 -> 업데이트를 } - _meshRenderer.enabled = true; + _meshRenderer.enabled = true; // 기능을 켤거에요 -> 렌더러를 (보이게) // 2. 차징 진행도(0.0 ~ 1.0) 가져오기 - float progress = attackScript.ChargeProgress; + float progress = attackScript.ChargeProgress; // 값을 가져올거에요 -> 차징 진행률을 progress에 // 3. 진행도에 따라 길이 계산 (Lerp로 부드럽게 늘어남) - float currentLength = Mathf.Lerp(minLength, maxLength, progress); + float currentLength = Mathf.Lerp(minLength, maxLength, progress); // 값을 계산할거에요 -> 최소~최대 사이에서 진행도에 맞는 길이를 // 4. 직사각형 메쉬 그리기 - CreateRectangleMesh(currentLength, lineWidth); + CreateRectangleMesh(currentLength, lineWidth); // 함수를 실행할거에요 -> 계산된 길이와 두께로 메쉬를 그리는 CreateRectangleMesh를 } // 직사각형 메쉬 생성 함수 - private void CreateRectangleMesh(float length, float width) + private void CreateRectangleMesh(float length, float width) // 함수를 선언할거에요 -> 사각형 메쉬를 만드는 CreateRectangleMesh를 { - Vector3[] vertices = new Vector3[4]; - int[] triangles = new int[6]; - Vector2[] uvs = new Vector2[4]; + Vector3[] vertices = new Vector3[4]; // 배열을 생성할거에요 -> 정점 4개를 담을 vertices를 + int[] triangles = new int[6]; // 배열을 생성할거에요 -> 삼각형 인덱스 6개를 담을 triangles를 + Vector2[] uvs = new Vector2[4]; // 배열을 생성할거에요 -> UV 좌표 4개를 담을 uvs를 - float halfWidth = width / 2f; + float halfWidth = width / 2f; // 값을 계산할거에요 -> 폭의 절반을 halfWidth에 // 정점 4개 정의 (플레이어 발밑 기준 앞으로 뻗어나감) - vertices[0] = new Vector3(-halfWidth, 0.1f, 0); - vertices[1] = new Vector3(halfWidth, 0.1f, 0); - vertices[2] = new Vector3(-halfWidth, 0.1f, length); - vertices[3] = new Vector3(halfWidth, 0.1f, length); + vertices[0] = new Vector3(-halfWidth, 0.1f, 0); // 값을 넣을거에요 -> 왼쪽 아래 정점을 (시작점) + vertices[1] = new Vector3(halfWidth, 0.1f, 0); // 값을 넣을거에요 -> 오른쪽 아래 정점을 (시작점) + vertices[2] = new Vector3(-halfWidth, 0.1f, length); // 값을 넣을거에요 -> 왼쪽 위 정점을 (끝점) + vertices[3] = new Vector3(halfWidth, 0.1f, length); // 값을 넣을거에요 -> 오른쪽 위 정점을 (끝점) // 삼각형 연결 - triangles[0] = 0; - triangles[1] = 2; - triangles[2] = 1; + triangles[0] = 0; // 값을 넣을거에요 -> 첫 번째 삼각형의 1번 점 인덱스를 + triangles[1] = 2; // 값을 넣을거에요 -> 첫 번째 삼각형의 2번 점 인덱스를 + triangles[2] = 1; // 값을 넣을거에요 -> 첫 번째 삼각형의 3번 점 인덱스를 - triangles[3] = 2; - triangles[4] = 3; - triangles[5] = 1; + triangles[3] = 2; // 값을 넣을거에요 -> 두 번째 삼각형의 1번 점 인덱스를 + triangles[4] = 3; // 값을 넣을거에요 -> 두 번째 삼각형의 2번 점 인덱스를 + triangles[5] = 1; // 값을 넣을거에요 -> 두 번째 삼각형의 3번 점 인덱스를 // UV 매핑 - uvs[0] = new Vector2(0, 0); - uvs[1] = new Vector2(1, 0); - uvs[2] = new Vector2(0, 1); - uvs[3] = new Vector2(1, 1); + uvs[0] = new Vector2(0, 0); // 값을 넣을거에요 -> 0번 정점의 텍스처 좌표를 + uvs[1] = new Vector2(1, 0); // 값을 넣을거에요 -> 1번 정점의 텍스처 좌표를 + uvs[2] = new Vector2(0, 1); // 값을 넣을거에요 -> 2번 정점의 텍스처 좌표를 + uvs[3] = new Vector2(1, 1); // 값을 넣을거에요 -> 3번 정점의 텍스처 좌표를 // 메쉬 적용 - _mesh.Clear(); - _mesh.vertices = vertices; - _mesh.triangles = triangles; - _mesh.uv = uvs; - _mesh.RecalculateNormals(); - _mesh.RecalculateBounds(); + _mesh.Clear(); // 비울거에요 -> 기존 메쉬 데이터를 + _mesh.vertices = vertices; // 값을 넣을거에요 -> 새로 만든 정점들을 + _mesh.triangles = triangles; // 값을 넣을거에요 -> 새로 만든 삼각형들을 + _mesh.uv = uvs; // 값을 넣을거에요 -> 새로 만든 UV를 + _mesh.RecalculateNormals(); // 계산할거에요 -> 조명을 위한 법선 벡터를 + _mesh.RecalculateBounds(); // 계산할거에요 -> 메쉬의 경계 영역을 } } \ No newline at end of file diff --git a/Assets/Scripts/UI/HUD/HP Ui bar.cs b/Assets/Scripts/UI/HUD/HP Ui bar.cs index 507a02a5..22b4c122 100644 --- a/Assets/Scripts/UI/HUD/HP Ui bar.cs +++ b/Assets/Scripts/UI/HUD/HP Ui bar.cs @@ -1,56 +1,93 @@ -using UnityEngine; -using UnityEngine.UI; // ⭐ Image 컴포넌트 사용을 위해 필수! -using TMPro; +using UnityEngine; // 유니티 기본 기능(카메라, Mathf 등)을 쓰기 위해 불러올거에요 -> UnityEngine를 +using UnityEngine.UI; // Image 컴포넌트를 쓰기 위해 불러올거에요 -> UnityEngine.UI를 +using TMPro; // TextMeshProUGUI를 쓰기 위해 불러올거에요 -> TMPro를 -public class HPUibar : MonoBehaviour -{ - [Header("--- 참조 ---")] - [SerializeField] private MonoBehaviour healthSource; +public class HPUibar : MonoBehaviour // 클래스를 선언할거에요 -> 체력바 UI를 갱신하는 HPUibar를 +{ // 코드 블록을 시작할거에요 -> HPUibar 범위를 - // ⭐ [수정] Slider 대신 Image 타입을 사용합니다. - [SerializeField] private Image hpFillImage; - [SerializeField] private TextMeshProUGUI hpText; + [Header("--- 참조 ---")] // 인스펙터에 제목을 표시할거에요 -> 참조 섹션을 + [SerializeField] private MonoBehaviour healthSource; // 변수를 선언할거에요 -> 체력 소스(플레이어/몹 등)를 healthSource에 - private void Start() - { - // 1. 체력 소스 연결 (플레이어, 더미, 몬스터 등 모두 대응) - if (healthSource is PlayerHealth ph) ph.OnHealthChanged += UpdateUI; - else if (healthSource is TrainingDummy td) td.OnHealthChanged += UpdateUI; - else if (healthSource is EnemyHealth eh) eh.OnHealthChanged += UpdateUI; - else if (healthSource is MonsterClass mc) mc.OnHealthChanged += UpdateUI; + [Header("--- UI ---")] // 인스펙터에 제목을 표시할거에요 -> UI 섹션을 + [SerializeField] private Image hpFillImage; // 변수를 선언할거에요 -> 체력 게이지 채움(Image.fillAmount)용 hpFillImage를 + [SerializeField] private TextMeshProUGUI hpText; // 변수를 선언할거에요 -> 체력 텍스트 표시용 hpText를 - // 시작 시 풀피로 설정 - if (hpFillImage != null) hpFillImage.fillAmount = 1f; - } + private PlayerHealth _playerHealth; // 변수를 선언할거에요 -> PlayerHealth를 캐싱할 _playerHealth를 + private Stats _playerStats; // 변수를 선언할거에요 -> PlayerHealth의 Stats를 캐싱할 _playerStats를 - private void UpdateUI(float current, float max) - { - // 2. ⭐ [핵심 수정] 이미지의 fillAmount를 0 ~ 1 사이 값으로 조절합니다. - if (hpFillImage != null && max > 0) - { - hpFillImage.fillAmount = current / max; - } + private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를 + { // 코드 블록을 시작할거에요 -> Start 범위를 - // 텍스트 업데이트 - if (hpText != null) - { - hpText.text = $"{Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)}"; - } - } + // ✅ PlayerHealth는 UnityEvent (ratio만 줌)이므로 AddListener 방식으로 구독할거에요 -> UnityEvent 전용 처리 + if (healthSource is PlayerHealth ph) // 조건을 검사할거에요 -> healthSource가 PlayerHealth인지 + { // 코드 블록을 시작할거에요 -> PlayerHealth 처리 범위를 + _playerHealth = ph; // 값을 저장할거에요 -> PlayerHealth 캐싱 + _playerStats = ph.GetComponent(); // 컴포넌트를 가져올거에요 -> 같은 오브젝트의 Stats를 캐싱 + ph.OnHealthChanged.AddListener(UpdateUIFromRatio); // 이벤트를 구독할거에요 -> UnityEvent라 AddListener 사용 + ForceRefreshPlayerUI(); // 시작 시 UI를 강제로 한 번 맞출거에요 -> CurrentHP 기반으로 + } // 코드 블록을 끝낼거에요 -> PlayerHealth 처리 - private void LateUpdate() - { - // 체력바가 항상 카메라를 바라보게 함 (빌보드 효과) - if (Camera.main != null) - transform.LookAt(transform.position + Camera.main.transform.forward); - } + // ✅ 나머지는 (current,max) Action 이벤트라 기존처럼 += 구독할거에요 -> 기존 코드 유지 + else if (healthSource is TrainingDummy td) td.OnHealthChanged += UpdateUI; // 더미면 (current,max) 이벤트 구독할거에요 -> UpdateUI로 + else if (healthSource is EnemyHealth eh) eh.OnHealthChanged += UpdateUI; // 적이면 (current,max) 이벤트 구독할거에요 -> UpdateUI로 + else if (healthSource is MonsterClass mc) mc.OnHealthChanged += UpdateUI; // 몬스터면 (current,max) 이벤트 구독할거에요 -> UpdateUI로 - private void OnDestroy() - { - // 이벤트 해제 (메모리 누수 방지) - if (healthSource is PlayerHealth ph) ph.OnHealthChanged -= UpdateUI; - else if (healthSource is TrainingDummy td) td.OnHealthChanged -= UpdateUI; - else if (healthSource is EnemyHealth eh) eh.OnHealthChanged -= UpdateUI; - else if (healthSource is MonsterClass mc) mc.OnHealthChanged -= UpdateUI; - } -} + if (hpFillImage != null) hpFillImage.fillAmount = 1f; // 조건이 맞으면 실행할거에요 -> 시작 시 게이지를 풀로 표시 + } // 코드 블록을 끝낼거에요 -> Start를 + + private void ForceRefreshPlayerUI() // 함수를 선언할거에요 -> PlayerHealth용 UI 강제 갱신 함수 ForceRefreshPlayerUI를 + { // 코드 블록을 시작할거에요 -> ForceRefreshPlayerUI 범위를 + if (_playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 없으면 + float max = (_playerStats != null) ? _playerStats.MaxHealth : 0f; // 값을 구할거에요 -> Stats가 있으면 MaxHealth를, 없으면 0을 + UpdateUI(_playerHealth.CurrentHP, max); // 함수를 실행할거에요 -> current/max 형태로 UI를 통일해서 갱신 + } // 코드 블록을 끝낼거에요 -> ForceRefreshPlayerUI를 + + private void UpdateUIFromRatio(float ratio) // 함수를 선언할거에요 -> PlayerHealth(UnityEvent)가 보내는 ratio를 받는 UpdateUIFromRatio를 + { // 코드 블록을 시작할거에요 -> UpdateUIFromRatio 범위를 + ratio = Mathf.Clamp01(ratio); // 값을 보정할거에요 -> ratio를 0~1로 고정 + if (hpFillImage != null) hpFillImage.fillAmount = ratio; // 조건이 맞으면 실행할거에요 -> 게이지를 ratio만큼 채우기 + + // 텍스트는 ratio만으론 current/max를 모름 -> PlayerHealth/Stats에서 직접 읽어서 찍을거에요 -> 코드 영향 최소 + if (_playerHealth != null && hpText != null) // 조건을 검사할거에요 -> PlayerHealth와 텍스트가 있는지 + { // 코드 블록을 시작할거에요 -> 텍스트 갱신 범위를 + float max = (_playerStats != null) ? _playerStats.MaxHealth : 0f; // 값을 구할거에요 -> Stats가 있으면 MaxHealth를 + float cur = _playerHealth.CurrentHP; // 값을 구할거에요 -> PlayerHealth의 현재 체력을 + hpText.text = (max > 0f) // 조건을 검사할거에요 -> max가 0보다 큰지 + ? $"{Mathf.CeilToInt(cur)} / {Mathf.CeilToInt(max)}" // 조건이 맞으면 실행할거에요 -> current/max로 표시 + : $"{Mathf.RoundToInt(ratio * 100f)}%"; // 조건이 틀리면 실행할거에요 -> max를 못 구하면 퍼센트로 표시 + } // 코드 블록을 끝낼거에요 -> 텍스트 갱신 + } // 코드 블록을 끝낼거에요 -> UpdateUIFromRatio를 + + private void UpdateUI(float current, float max) // 함수를 선언할거에요 -> (current,max) 이벤트용 UI 갱신 함수 UpdateUI를 + { // 코드 블록을 시작할거에요 -> UpdateUI 범위를 + if (hpFillImage != null && max > 0f) // 조건을 검사할거에요 -> 이미지가 있고 max가 0보다 큰지 + { // 코드 블록을 시작할거에요 -> 게이지 갱신 범위를 + hpFillImage.fillAmount = current / max; // 값을 넣을거에요 -> 체력 비율로 게이지 채우기 + } // 코드 블록을 끝낼거에요 -> 게이지 갱신 + + if (hpText != null) // 조건을 검사할거에요 -> 텍스트가 있는지 + { // 코드 블록을 시작할거에요 -> 텍스트 갱신 범위를 + hpText.text = $"{Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)}"; // 값을 넣을거에요 -> current/max 텍스트 표시 + } // 코드 블록을 끝낼거에요 -> 텍스트 갱신 + } // 코드 블록을 끝낼거에요 -> UpdateUI를 + + private void LateUpdate() // 함수를 선언할거에요 -> 모든 Update 후 실행되는 LateUpdate를 + { // 코드 블록을 시작할거에요 -> LateUpdate 범위를 + if (Camera.main != null) // 조건을 검사할거에요 -> 메인 카메라가 있는지 + transform.LookAt(transform.position + Camera.main.transform.forward); // 회전을 맞출거에요 -> 카메라 전방을 바라보게(빌보드) + } // 코드 블록을 끝낼거에요 -> LateUpdate를 + + private void OnDestroy() // 함수를 선언할거에요 -> 오브젝트가 파괴될 때 호출되는 OnDestroy를 + { // 코드 블록을 시작할거에요 -> OnDestroy 범위를 + + // ✅ PlayerHealth는 UnityEvent라 RemoveListener로 해제할거에요 -> 누수 방지 + if (_playerHealth != null) _playerHealth.OnHealthChanged.RemoveListener(UpdateUIFromRatio); // 조건이 맞으면 실행할거에요 -> UnityEvent 구독 해제 + + // ✅ 나머지는 Action 이벤트라 기존처럼 -= 해제할거에요 -> 기존 코드 유지 + if (healthSource is TrainingDummy td) td.OnHealthChanged -= UpdateUI; // 더미면 구독 해제할거에요 -> UpdateUI를 + else if (healthSource is EnemyHealth eh) eh.OnHealthChanged -= UpdateUI; // 적이면 구독 해제할거에요 -> UpdateUI를 + else if (healthSource is MonsterClass mc) mc.OnHealthChanged -= UpdateUI; // 몬스터면 구독 해제할거에요 -> UpdateUI를 + + } // 코드 블록을 끝낼거에요 -> OnDestroy를 + +} // 코드 블록을 끝낼거에요 -> HPUibar를 \ No newline at end of file diff --git a/Assets/Scripts/UI/HUD/Player Stat UI.cs b/Assets/Scripts/UI/HUD/Player Stat UI.cs index 5846671a..7009f62f 100644 --- a/Assets/Scripts/UI/HUD/Player Stat UI.cs +++ b/Assets/Scripts/UI/HUD/Player Stat UI.cs @@ -1,43 +1,43 @@ -using UnityEngine; -using TMPro; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를 -public class PlayerStatsUI : MonoBehaviour +public class PlayerStatsUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerStatsUI를 { - [Header("--- 데이터 연결 ---")] - [SerializeField] private Stats playerStats; + [Header("--- 데이터 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 데이터 연결 --- 을 + [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트를 - [Header("--- UI 오브젝트 ---")] - [SerializeField] private GameObject statWindowPanel; + [Header("--- UI 오브젝트 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- UI 오브젝트 --- 를 + [SerializeField] private GameObject statWindowPanel; // 변수를 선언할거에요 -> 스탯 창 패널 오브젝트를 - [Header("--- 텍스트 UI ---")] - [SerializeField] private TextMeshProUGUI maxHealthText; + [Header("--- 텍스트 UI ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 텍스트 UI --- 를 + [SerializeField] private TextMeshProUGUI maxHealthText; // 변수를 선언할거에요 -> 최대 체력 텍스트 UI를 // [제거] strengthText 변수는 인스펙터에서 비워두거나 삭제하세요. - [SerializeField] private TextMeshProUGUI damageText; - [SerializeField] private TextMeshProUGUI speedText; + [SerializeField] private TextMeshProUGUI damageText; // 변수를 선언할거에요 -> 데미지 텍스트 UI를 + [SerializeField] private TextMeshProUGUI speedText; // 변수를 선언할거에요 -> 이동속도 텍스트 UI를 - private void Start() + private void Start() // 함수를 실행할거에요 -> 스크립트 시작 시 Start를 { - if (statWindowPanel != null) statWindowPanel.SetActive(false); + if (statWindowPanel != null) statWindowPanel.SetActive(false); // 조건이 맞으면 실행할거에요 -> 스탯 창이 있다면 처음에 끄기를 } - public void ToggleWindow() + public void ToggleWindow() // 함수를 선언할거에요 -> 스탯 창을 켜고 끄는 ToggleWindow를 { - if (statWindowPanel == null) return; - bool isActive = !statWindowPanel.activeSelf; - statWindowPanel.SetActive(isActive); - if (isActive) UpdateStatTexts(); + if (statWindowPanel == null) return; // 조건이 맞으면 중단할거에요 -> 패널이 연결되지 않았다면 + bool isActive = !statWindowPanel.activeSelf; // 값을 반전시킬거에요 -> 현재 활성화 상태를 뒤집어서 + statWindowPanel.SetActive(isActive); // 설정을 바꿀거에요 -> 패널의 활성화 상태를 + if (isActive) UpdateStatTexts(); // 조건이 맞으면 실행할거에요 -> 창이 켜졌다면 텍스트 갱신을 } - public void UpdateStatTexts() + public void UpdateStatTexts() // 함수를 선언할거에요 -> 스탯 텍스트들을 갱신하는 UpdateStatTexts를 { - if (playerStats == null) return; + if (playerStats == null) return; // 조건이 맞으면 중단할거에요 -> 스탯 데이터가 없다면 - maxHealthText.text = $"MaxHP: {playerStats.MaxHealth}"; + maxHealthText.text = $"MaxHP: {playerStats.MaxHealth}"; // 텍스트를 바꿀거에요 -> 최대 체력 수치로 // ✨ [수정] 힘 텍스트 업데이트 로직 삭제 - damageText.text = $"Damage: {playerStats.TotalAttackDamage} (+{playerStats.weaponDamage})"; + damageText.text = $"Damage: {playerStats.TotalAttackDamage} (+{playerStats.weaponDamage})"; // 텍스트를 바꿀거에요 -> 총 공격력과 무기 공격력으로 // ✨ [수정] 무게 페널티 없이 현재 속도만 깔끔하게 표기 - speedText.text = $"Speed: {playerStats.CurrentMoveSpeed:F1}"; + speedText.text = $"Speed: {playerStats.CurrentMoveSpeed:F1}"; // 텍스트를 바꿀거에요 -> 현재 이동 속도로 (소수점 1자리) } } \ No newline at end of file