모스 몬스터

This commit is contained in:
윤기주_playm 2026-02-13 00:23:25 +09:00
parent e126460285
commit 2a4c20435f
77 changed files with 5371 additions and 4125 deletions

BIN
.plastic/plastic.changes Normal file

Binary file not shown.

Binary file not shown.

View File

@ -4383,6 +4383,74 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4708095961488610, guid: 01d14f7c3a49b4f4e86caee63ef815b1, type: 3} m_CorrespondingSourceObject: {fileID: 4708095961488610, guid: 01d14f7c3a49b4f4e86caee63ef815b1, type: 3}
m_PrefabInstance: {fileID: 47955374} m_PrefabInstance: {fileID: 47955374}
m_PrefabAsset: {fileID: 0} 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 --- !u!1001 &48315642
PrefabInstance: PrefabInstance:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -9052,14 +9120,14 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 85056388} m_GameObject: {fileID: 85056388}
m_Enabled: 1 m_Enabled: 0
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3} m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
spawnRange: 15 spawnRange: 15
respawnCooldown: 3 respawnCooldown: 3
mobTag: Boss mobTag: Throw Monster
optimizationRange: 60 optimizationRange: 60
--- !u!4 &85056390 --- !u!4 &85056390
Transform: Transform:
@ -39085,9 +39153,21 @@ MonoBehaviour:
impactSpawnPoint: {fileID: 0} impactSpawnPoint: {fileID: 0}
counterSystem: {fileID: 358757530} counterSystem: {fileID: 358757530}
patternInterval: 3 patternInterval: 3
ironBall: {fileID: 0} attackRange: 3
handHolder: {fileID: 0} ironBall: {fileID: 1550577165}
handHolder: {fileID: 1325714516}
bossHealthBar: {fileID: 0} 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 --- !u!136 &358757532
CapsuleCollider: CapsuleCollider:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -39158,7 +39238,7 @@ Rigidbody:
m_UseGravity: 1 m_UseGravity: 1
m_IsKinematic: 1 m_IsKinematic: 1
m_Interpolate: 0 m_Interpolate: 0
m_Constraints: 0 m_Constraints: 80
m_CollisionDetection: 0 m_CollisionDetection: 0
--- !u!1001 &358842824 --- !u!1001 &358842824
PrefabInstance: PrefabInstance:
@ -66018,7 +66098,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 1 m_IsActive: 0
--- !u!114 &600231663 --- !u!114 &600231663
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -66451,8 +66531,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3} m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
healthSource: {fileID: 2112919314} healthSource: {fileID: 0}
hpFillImage: {fileID: 910004995} hpFillImage: {fileID: 0}
hpText: {fileID: 0} hpText: {fileID: 0}
--- !u!1001 &603601734 --- !u!1001 &603601734
PrefabInstance: PrefabInstance:
@ -68541,6 +68621,11 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4012526298001850, guid: 1b1f948f132c1cc429df46b11756f4a2, type: 3} m_CorrespondingSourceObject: {fileID: 4012526298001850, guid: 1b1f948f132c1cc429df46b11756f4a2, type: 3}
m_PrefabInstance: {fileID: 619812415} m_PrefabInstance: {fileID: 619812415}
m_PrefabAsset: {fileID: 0} 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 --- !u!1 &620650552
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -115622,6 +115707,74 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4872071498859388, guid: 5a0d3c35fdb1a0549ab15f2d38859ed1, type: 3} m_CorrespondingSourceObject: {fileID: 4872071498859388, guid: 5a0d3c35fdb1a0549ab15f2d38859ed1, type: 3}
m_PrefabInstance: {fileID: 1080970801} m_PrefabInstance: {fileID: 1080970801}
m_PrefabAsset: {fileID: 0} 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 --- !u!1001 &1083084820
PrefabInstance: PrefabInstance:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -118888,17 +119041,29 @@ PrefabInstance:
serializedVersion: 3 serializedVersion: 3
m_TransformParent: {fileID: 0} m_TransformParent: {fileID: 0}
m_Modifications: 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} - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
propertyPath: m_LocalPosition.x propertyPath: m_LocalPosition.x
value: 28.143673 value: 27.059
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
propertyPath: m_LocalPosition.y propertyPath: m_LocalPosition.y
value: 5.500213 value: 5.48
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
propertyPath: m_LocalPosition.z propertyPath: m_LocalPosition.z
value: 32.754795 value: 33.461
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
propertyPath: m_LocalRotation.w propertyPath: m_LocalRotation.w
@ -118932,13 +119097,24 @@ PrefabInstance:
propertyPath: m_Name propertyPath: m_Name
value: BossMonster 3D model value: BossMonster 3D model
objectReference: {fileID: 0} 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} - target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
propertyPath: m_Controller propertyPath: m_Controller
value: value:
objectReference: {fileID: 9100000, guid: b207531287111e74d890e730f7278dfa, type: 2} 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_RemovedComponents: []
m_RemovedGameObjects: [] m_RemovedGameObjects: []
m_AddedGameObjects: [] m_AddedGameObjects:
- targetCorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
insertIndex: -1
addedObject: {fileID: 1325714516}
m_AddedComponents: m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3} - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
insertIndex: -1 insertIndex: -1
@ -142858,13 +143034,13 @@ Transform:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1325714515} m_GameObject: {fileID: 1325714515}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: -0.0026552675, y: 0.03898671, z: 0.72380346, w: -0.6888988} m_LocalRotation: {x: -0.038344428, y: 0.70765644, z: -0.0024401005, w: -0.7055112}
m_LocalPosition: {x: 30.757803, y: 8.46146, z: 31.390688} m_LocalPosition: {x: 1.304071, y: 1.8877394, z: -1.5619158}
m_LocalScale: {x: 234.34998, y: 234.35007, z: 234.35002} m_LocalScale: {x: 234.34999, y: 234.35004, z: 234.35002}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 1550577166} - {fileID: 1550577166}
m_Father: {fileID: 0} m_Father: {fileID: 620355262}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &1326407535 --- !u!1001 &1326407535
PrefabInstance: PrefabInstance:
@ -165537,8 +165713,8 @@ Transform:
m_GameObject: {fileID: 1550577165} m_GameObject: {fileID: 1550577165}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0.002655249, y: -0.03898664, z: -0.72380346, w: -0.68889886} 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_LocalPosition: {x: 0.00886, y: -0.00478, z: 0.0054}
m_LocalScale: {x: 0.0090075, y: 0.009007501, z: 0.0090075} m_LocalScale: {x: 0.0055073895, y: 0.0055073905, z: 0.0055073895}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
m_Father: {fileID: 1325714516} m_Father: {fileID: 1325714516}
@ -228647,6 +228823,22 @@ PrefabInstance:
propertyPath: minLength propertyPath: minLength
value: 0.2 value: 0.2
objectReference: {fileID: 0} 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} - target: {fileID: 8345435216483992478, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
propertyPath: m_SlopeLimit propertyPath: m_SlopeLimit
value: 47.82 value: 47.82
@ -228659,6 +228851,10 @@ PrefabInstance:
propertyPath: normalRange propertyPath: normalRange
value: 15 value: 15
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 8965661853020836870, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
propertyPath: playerStats
value:
objectReference: {fileID: 1432447530}
- target: {fileID: 8965661853020836870, guid: c34da37720b95c84887eda34e2d90e5b, type: 3} - target: {fileID: 8965661853020836870, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
propertyPath: weaponHitBox propertyPath: weaponHitBox
value: value:
@ -229299,9 +229495,10 @@ SceneRoots:
- {fileID: 2067270637} - {fileID: 2067270637}
- {fileID: 615584948} - {fileID: 615584948}
- {fileID: 200768984} - {fileID: 200768984}
- {fileID: 1325714516}
- {fileID: 1759137305} - {fileID: 1759137305}
- {fileID: 198901434} - {fileID: 198901434}
- {fileID: 929255795} - {fileID: 929255795}
- {fileID: 1844125250} - {fileID: 1844125250}
- {fileID: 1101415473} - {fileID: 1101415473}
- {fileID: 1081393079}
- {fileID: 48215575}

View File

@ -8,7 +8,13 @@ AnimatorController:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_Name: BossMonsterAnimater m_Name: BossMonsterAnimater
serializedVersion: 5 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: m_AnimatorLayers:
- serializedVersion: 5 - serializedVersion: 5
m_Name: Base Layer m_Name: Base Layer

View File

@ -1231,6 +1231,9 @@ PrefabInstance:
insertIndex: -1 insertIndex: -1
addedObject: {fileID: 7115393052906583336} addedObject: {fileID: 7115393052906583336}
m_AddedComponents: m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
insertIndex: -1
addedObject: {fileID: 8160405634931995834}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
insertIndex: -1 insertIndex: -1
addedObject: {fileID: -903076858900737616} addedObject: {fileID: -903076858900737616}
@ -1243,9 +1246,6 @@ PrefabInstance:
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
insertIndex: -1 insertIndex: -1
addedObject: {fileID: 8965661853020836870} addedObject: {fileID: 8965661853020836870}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
insertIndex: -1
addedObject: {fileID: 8160405634931995834}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
insertIndex: -1 insertIndex: -1
addedObject: {fileID: 7686364893196566162} addedObject: {fileID: 7686364893196566162}
@ -1273,6 +1273,28 @@ GameObject:
m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
m_PrefabInstance: {fileID: 1112926104530361804} m_PrefabInstance: {fileID: 1112926104530361804}
m_PrefabAsset: {fileID: 0} 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 --- !u!114 &-903076858900737616
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -1368,23 +1390,6 @@ MonoBehaviour:
m_Bits: 0 m_Bits: 0
onlyMaxCharge: 1 onlyMaxCharge: 1
defaultProjectilePrefab: {fileID: 0} 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 --- !u!114 &7686364893196566162
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

BIN
Assets/3D Assets/BossKK.fbx Normal file

Binary file not shown.

View File

@ -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:

View File

@ -16,8 +16,7 @@ ModelImporter:
optimizeGameObjects: 0 optimizeGameObjects: 0
removeConstantScaleCurves: 0 removeConstantScaleCurves: 0
motionNodeName: motionNodeName:
rigImportErrors: "Copied Avatar Rig Configuration mis-match. Transform hierarchy rigImportErrors:
does not match:\n\tTransform 'spine' for human bone 'Hips' not found\n"
rigImportWarnings: rigImportWarnings:
animationImportErrors: animationImportErrors:
animationImportWarnings: animationImportWarnings:
@ -32,7 +31,36 @@ ModelImporter:
animationWrapMode: 0 animationWrapMode: 0
extraExposedTransformPaths: [] extraExposedTransformPaths: []
extraUserProperties: [] 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 isReadable: 0
meshes: meshes:
lODScreenPercentages: [] lODScreenPercentages: []
@ -82,8 +110,402 @@ ModelImporter:
importAnimation: 1 importAnimation: 1
humanDescription: humanDescription:
serializedVersion: 3 serializedVersion: 3
human: [] human:
skeleton: [] - 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 armTwist: 0.5
foreArmTwist: 0.5 foreArmTwist: 0.5
upperLegTwist: 0.5 upperLegTwist: 0.5

View File

@ -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:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bff4e8c104f01194fb159c6c309b3a49
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -16,8 +16,7 @@ ModelImporter:
optimizeGameObjects: 0 optimizeGameObjects: 0
removeConstantScaleCurves: 0 removeConstantScaleCurves: 0
motionNodeName: motionNodeName:
rigImportErrors: "Copied Avatar Rig Configuration mis-match. Transform hierarchy rigImportErrors:
does not match:\n\tTransform 'spine' for human bone 'Hips' not found\n"
rigImportWarnings: rigImportWarnings:
animationImportErrors: animationImportErrors:
animationImportWarnings: animationImportWarnings:
@ -32,7 +31,36 @@ ModelImporter:
animationWrapMode: 0 animationWrapMode: 0
extraExposedTransformPaths: [] extraExposedTransformPaths: []
extraUserProperties: [] 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 isReadable: 0
meshes: meshes:
lODScreenPercentages: [] lODScreenPercentages: []
@ -82,8 +110,402 @@ ModelImporter:
importAnimation: 1 importAnimation: 1
humanDescription: humanDescription:
serializedVersion: 3 serializedVersion: 3
human: [] human:
skeleton: [] - 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 armTwist: 0.5
foreArmTwist: 0.5 foreArmTwist: 0.5
upperLegTwist: 0.5 upperLegTwist: 0.5

View File

@ -1,78 +1,80 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능(Time, Vector3, Coroutine 등)을 쓰기 위해 불러올거에요 -> UnityEngine를
using Cinemachine; using Cinemachine; // 시네머신 카메라/임펄스 기능을 쓰기 위해 불러올거에요 -> Cinemachine을
using System.Collections; using System.Collections; // 코루틴(IEnumerator)을 쓰기 위해 불러올거에요 -> System.Collections를
public class CinemachineShake : MonoBehaviour public class CinemachineShake : MonoBehaviour // 클래스를 선언할거에요 -> 카메라 흔들림/줌/히트슬로우를 담당하는 CinemachineShake를
{ { // 코드 블록을 시작할거에요 -> CinemachineShake 범위를
public static CinemachineShake Instance { get; private set; }
[Header("--- 컴포넌트 연결 ---")] public static CinemachineShake Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스를 외부에서 읽기만 가능하게
private CinemachineImpulseSource _impulseSource;
[SerializeField] private CinemachineVirtualCamera _vCam;
[Header("--- 줌 설정 ---")] [Header("--- 컴포넌트 연결 ---")] // 인스펙터에 제목을 표시할거에요 -> 컴포넌트 연결 섹션을
[SerializeField] private float defaultFOV = 60f; private CinemachineImpulseSource _impulseSource; // 변수를 선언할거에요 -> 임펄스(카메라 흔들림) 발사기 컴포넌트를 담을 _impulseSource를
[SerializeField] private float zoomedFOV = 45f; [SerializeField] private CinemachineVirtualCamera _vCam; // 변수를 선언할거에요 -> 조절할 가상 카메라를 담을 _vCam을
[SerializeField] private float zoomSpeed = 5f;
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() private Coroutine _zoomCoroutine; // 변수를 선언할거에요 -> 현재 실행 중인 줌 코루틴을 담을 _zoomCoroutine을
{
Instance = this;
_impulseSource = GetComponent<CinemachineImpulseSource>();
if (_vCam == null) _vCam = GetComponent<CinemachineVirtualCamera>();
}
// ⭐ 렉 없는 타격감: 시간을 0.1배속으로 늘림 (Hit-Slow) private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를
public void HitSlow(float duration = 0.15f, float timeScale = 0.1f) { // 코드 블록을 시작할거에요 -> Awake 범위를
{ Instance = this; // 값을 넣을거에요 -> 싱글톤 인스턴스를 자기 자신으로 설정
StartCoroutine(DoHitSlow(duration, timeScale)); _impulseSource = GetComponent<CinemachineImpulseSource>(); // 컴포넌트를 가져올거에요 -> 내 오브젝트에서 임펄스 소스를 찾아 저장
} if (_vCam == null) _vCam = GetComponent<CinemachineVirtualCamera>(); // 조건이 맞으면 실행할거에요 -> vCam이 비어있으면 내 오브젝트에서 찾아 채우기
} // 코드 블록을 끝낼거에요 -> Awake를
private IEnumerator DoHitSlow(float duration, float timeScale) // ⭐ 렉 없는 타격감: 시간을 0.1배속으로 늘림 (Hit-Slow) // 설명을 적을거에요 -> 짧게 슬로모션을 걸어 타격감을 올리는 기능
{ public void HitSlow(float duration = 0.15f, float timeScale = 0.1f) // 함수를 선언할거에요 -> duration/timeScale로 히트슬로우를 주는 HitSlow를
Time.timeScale = timeScale; // 멈추지 않고 아주 느리게 { // 코드 블록을 시작할거에요 -> HitSlow 범위를
yield return new WaitForSecondsRealtime(duration); StartCoroutine(DoHitSlow(duration, timeScale)); // 코루틴을 실행할거에요 -> 실제 슬로우 처리를 하는 DoHitSlow를
Time.timeScale = 1.0f; } // 코드 블록을 끝낼거에요 -> HitSlow를
}
// ⭐ 카메라 킥: 순간적으로 FOV를 확 줄여 충격을 줌 private IEnumerator DoHitSlow(float duration, float timeScale) // 코루틴 함수를 선언할거에요 -> 히트슬로우를 수행하는 DoHitSlow를
public void CameraKick(float kickAmount = 8f) { // 코드 블록을 시작할거에요 -> DoHitSlow 범위를
{ Time.timeScale = timeScale; // 값을 바꿀거에요 -> 전체 게임 시간을 느리게(멈춤은 아님)
if (_vCam == null) return; yield return new WaitForSecondsRealtime(duration); // 기다릴거에요 -> 실제 시간 기준으로 duration만큼
_vCam.m_Lens.FieldOfView -= kickAmount; // 순간 줌인 Time.timeScale = 1.0f; // 값을 복구할거에요 -> 게임 시간을 정상(1)으로
SetZoom(false); // AnimateZoom을 통해 부드럽게 복구 } // 코드 블록을 끝낼거에요 -> DoHitSlow를
}
public void ShakeAttack() // ⭐ 카메라 킥: 순간적으로 FOV를 확 줄여 충격을 줌 // 설명을 적을거에요 -> 순간 줌인 후 부드럽게 복구시키는 연출
{ public void CameraKick(float kickAmount = 8f) // 함수를 선언할거에요 -> FOV를 순간 감소시키는 CameraKick을
if (_impulseSource == null) return; { // 코드 블록을 시작할거에요 -> CameraKick 범위를
_impulseSource.GenerateImpulse(Vector3.down * 1.5f); if (_vCam == null) return; // 조건이 맞으면 종료할거에요 -> 카메라가 없으면 처리 불가
} _vCam.m_Lens.FieldOfView -= kickAmount; // 값을 바꿀거에요 -> FOV를 줄여서 순간 줌인 효과
SetZoom(false); // 함수를 실행할거에요 -> 기본 FOV로 부드럽게 돌아가도록 줌 애니메이션 시작
} // 코드 블록을 끝낼거에요 -> CameraKick을
public void ShakeNoNo() public void ShakeAttack() // 함수를 선언할거에요 -> 공격용 흔들림을 주는 ShakeAttack을
{ { // 코드 블록을 시작할거에요 -> ShakeAttack 범위를
if (_impulseSource == null) return; if (_impulseSource == null) return; // 조건이 맞으면 종료할거에요 -> 임펄스 소스가 없으면 불가
_impulseSource.GenerateImpulse(Vector3.right * 0.4f); _impulseSource.GenerateImpulse(Vector3.down * 1.5f); // 임펄스를 발생시킬거에요 -> 아래 방향으로 강하게 흔들기
} } // 코드 블록을 끝낼거에요 -> ShakeAttack을
public void SetZoom(bool isZooming) public void ShakeNoNo() // 함수를 선언할거에요 -> 좌우로 살짝 흔드는 ShakeNoNo를
{ { // 코드 블록을 시작할거에요 -> ShakeNoNo 범위를
if (_vCam == null) return; if (_impulseSource == null) return; // 조건이 맞으면 종료할거에요 -> 임펄스 소스가 없으면 불가
if (_zoomCoroutine != null) StopCoroutine(_zoomCoroutine); _impulseSource.GenerateImpulse(Vector3.right * 0.4f); // 임펄스를 발생시킬거에요 -> 오른쪽 방향으로 약하게 흔들기
float targetFOV = isZooming ? zoomedFOV : defaultFOV; } // 코드 블록을 끝낼거에요 -> ShakeNoNo를
_zoomCoroutine = StartCoroutine(AnimateZoom(targetFOV));
}
private IEnumerator AnimateZoom(float target) public void SetZoom(bool isZooming) // 함수를 선언할거에요 -> 줌인/줌아웃 목표를 설정하는 SetZoom을
{ { // 코드 블록을 시작할거에요 -> SetZoom 범위를
if (_vCam == null) yield break; if (_vCam == null) return; // 조건이 맞으면 종료할거에요 -> 카메라가 없으면 불가
while (Mathf.Abs(_vCam.m_Lens.FieldOfView - target) > 0.1f) if (_zoomCoroutine != null) StopCoroutine(_zoomCoroutine); // 조건이 맞으면 실행할거에요 -> 기존 줌 코루틴이 있으면 중지해서 중첩 방지
{ float targetFOV = isZooming ? zoomedFOV : defaultFOV; // 목표 FOV를 정할거에요 -> 줌이면 zoomedFOV, 아니면 defaultFOV
_vCam.m_Lens.FieldOfView = Mathf.Lerp(_vCam.m_Lens.FieldOfView, target, Time.deltaTime * zoomSpeed); _zoomCoroutine = StartCoroutine(AnimateZoom(targetFOV)); // 코루틴을 실행할거에요 -> 목표 FOV로 부드럽게 이동하는 AnimateZoom을
yield return null; } // 코드 블록을 끝낼거에요 -> SetZoom을
}
_vCam.m_Lens.FieldOfView = target; 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를

View File

@ -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("회전 설정")] [Header("회전 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 회전 설정 을
[SerializeField] private float rotationSpeed = 15f; // 감도 조절 [SerializeField] private float rotationSpeed = 15f; // 변수를 선언할거에요 -> 회전 속도(감도)를
[SerializeField] private float rotationDeadzone = 1.2f; // 근접전 '뒤돌기' 방지 범위 [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 좌표로 변환합니다. // 1. 캐릭터의 월드 위치를 화면 상의 2D 좌표로 변환합니다.
Vector3 playerScreenPos = Camera.main.WorldToScreenPoint(transform.position); Vector3 playerScreenPos = Camera.main.WorldToScreenPoint(transform.position); // 변환할거에요 -> 캐릭터 월드 좌표를 화면 좌표로
// 2. 마우스의 화면 좌표를 가져옵니다. // 2. 마우스의 화면 좌표를 가져옵니다.
Vector3 mousePos = Input.mousePosition; Vector3 mousePos = Input.mousePosition; // 가져올거에요 -> 현재 마우스 위치를
// 3. 화면상에서 플레이어 -> 마우스로 향하는 2D 방향 벡터를 구합니다. // 3. 화면상에서 플레이어 -> 마우스로 향하는 2D 방향 벡터를 구합니다.
Vector3 dir = mousePos - playerScreenPos; Vector3 dir = mousePos - playerScreenPos; // 계산할거에요 -> 마우스와 플레이어 사이의 방향 벡터를
// 4. 거리가 너무 가까우면 회전을 무시합니다 (데드존 적용). // 4. 거리가 너무 가까우면 회전을 무시합니다 (데드존 적용).
if (dir.magnitude < rotationDeadzone * 50f) return; if (dir.magnitude < rotationDeadzone * 50f) return; // 조건이 맞으면 중단할거에요 -> 마우스가 캐릭터에 너무 가깝다면
// 5. 2D 방향 벡터를 각도(Atan2)로 변환합니다. // 5. 2D 방향 벡터를 각도(Atan2)로 변환합니다.
// 화면의 X는 월드의 X, 화면의 Y는 월드의 Z에 대응합니다 (쿼터뷰/탑다운 기준). // 화면의 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. 계산된 각도로 캐릭터를 부드럽게 회전시킵니다. // 6. 계산된 각도로 캐릭터를 부드럽게 회전시킵니다.
Quaternion targetRotation = Quaternion.Euler(0, angle, 0); Quaternion targetRotation = Quaternion.Euler(0, angle, 0); // 생성할거에요 -> 목표 회전값(쿼터니언)을
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); // 회전시킬거에요 -> 현재 회전에서 목표 회전으로 부드럽게
} }
} }

View File

@ -1,58 +1,60 @@
using UnityEngine; using UnityEngine; // Random.Range 같은 유니티 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
public class RandomStatCardInstance public class RandomStatCardInstance // 클래스를 선언할거에요 -> 랜덤 스탯 카드 1장의 동작/상태를 담당하는 RandomStatCardInstance를
{ { // 코드 블록을 시작할거에요 -> RandomStatCardInstance 범위를
private RandomStatCardData data;
private StatType statType;
private int value;
private bool isConfirmed = false;
private Stats stats;
public RandomStatCardInstance(RandomStatCardData data, Stats stats) private RandomStatCardData data; // 변수를 선언할거에요 -> 카드 데이터(가능 스탯/범위)를 담을 data를
{ private StatType statType; // 변수를 선언할거에요 -> 이번에 뽑힌 스탯 종류를 담을 statType을
this.data = data; private int value; // 변수를 선언할거에요 -> 이번에 뽑힌 수치를 담을 value를
this.stats = stats; private bool isConfirmed = false; // 변수를 선언할거에요 -> 이미 적용 확정했는지 여부를 isConfirmed에(기본 false)
} private Stats stats; // 변수를 선언할거에요 -> 실제 스탯을 적용할 대상(Stats)을 stats에
public void RollPreview() public RandomStatCardInstance(RandomStatCardData data, Stats stats) // 생성자를 선언할거에요 -> 데이터와 적용 대상을 받아 초기화하는 생성자를
{ { // 코드 블록을 시작할거에요 -> 생성자 범위를
statType = data.possibleStats[Random.Range(0, data.possibleStats.Length)]; this.data = data; // 값을 넣을거에요 -> 전달받은 data를 멤버 data에 저장
value = Random.Range(data.minValue, data.maxValue + 1); this.stats = stats; // 값을 넣을거에요 -> 전달받은 stats를 멤버 stats에 저장
isConfirmed = false; } // 코드 블록을 끝낼거에요 -> 생성자를
}
public string GetText() public void RollPreview() // 함수를 선언할거에요 -> 카드 미리보기(랜덤 결과) 뽑는 RollPreview를
{ { // 코드 블록을 시작할거에요 -> RollPreview 범위를
string sign = value >= 0 ? "+" : ""; statType = data.possibleStats[Random.Range(0, data.possibleStats.Length)]; // 값을 뽑을거에요 -> 가능한 스탯 중 하나를 랜덤으로
return $"{statType} {sign}{value}"; value = Random.Range(data.minValue, data.maxValue + 1); // 값을 뽑을거에요 -> min~max 범위에서 랜덤 수치를(+1은 int 상한 포함)
} isConfirmed = false; // 상태를 바꿀거에요 -> 아직 확정 전(false)로
} // 코드 블록을 끝낼거에요 -> RollPreview를
public void Confirm() public string GetText() // 함수를 선언할거에요 -> UI에 보여줄 텍스트를 만드는 GetText를
{ { // 코드 블록을 시작할거에요 -> GetText 범위를
if (isConfirmed) return; string sign = value >= 0 ? "+" : ""; // 값을 정할거에요 -> 양수면 + 표시, 음수면 빈 문자열로
ApplyStat(); return $"{statType} {sign}{value}"; // 문자열을 반환할거에요 -> "스탯 +값" 형태로
isConfirmed = true; } // 코드 블록을 끝낼거에요 -> GetText를
}
private void ApplyStat() public void Confirm() // 함수를 선언할거에요 -> 선택 확정 시 실제 적용하는 Confirm을
{ { // 코드 블록을 시작할거에요 -> Confirm 범위를
if (stats == null) return; if (isConfirmed) return; // 조건이 맞으면 종료할거에요 -> 이미 확정했으면 중복 적용 방지
ApplyStat(); // 함수를 실행할거에요 -> 뽑힌 스탯을 실제로 적용
isConfirmed = true; // 상태를 바꿀거에요 -> 이제 확정 완료(true)로
} // 코드 블록을 끝낼거에요 -> Confirm을
switch (statType) private void ApplyStat() // 함수를 선언할거에요 -> 스탯 적용 로직을 처리하는 ApplyStat을
{ { // 코드 블록을 시작할거에요 -> ApplyStat 범위를
case StatType.Health: if (stats == null) return; // 조건이 맞으면 종료할거에요 -> 적용 대상이 없으면 아무것도 안 함
stats.AddMaxHealth(value);
break;
case StatType.Speed: switch (statType) // 분기할거에요 -> 어떤 스탯인지에 따라
stats.AddMoveSpeed(value); { // 코드 블록을 시작할거에요 -> switch 범위를
break; case StatType.Health: // 조건을 처리할거에요 -> 체력 스탯이면
stats.AddMaxHealth(value); // 함수를 실행할거에요 -> 최대 체력에 value만큼 더하기
break; // 분기를 끝낼거에요 -> switch 탈출
// ✨ [제거] case StatType.Strength 삭제됨 case StatType.Speed: // 조건을 처리할거에요 -> 이동속도 스탯이면
stats.AddMoveSpeed(value); // 함수를 실행할거에요 -> 이동속도에 value만큼 더하기
break; // 분기를 끝낼거에요 -> switch 탈출
case StatType.Damage: // ✨ [제거] case StatType.Strength 삭제됨 // 설명을 적을거에요 -> 힘 스탯은 더 이상 처리하지 않음
stats.AddAttackDamage(value);
break; case StatType.Damage: // 조건을 처리할거에요 -> 공격력 스탯이면
} stats.AddAttackDamage(value); // 함수를 실행할거에요 -> 공격 데미지에 value만큼 더하기
} break; // 분기를 끝낼거에요 -> switch 탈출
} } // 코드 블록을 끝낼거에요 -> switch를
} // 코드 블록을 끝낼거에요 -> ApplyStat을
} // 코드 블록을 끝낼거에요 -> RandomStatCardInstance를

View File

@ -1,138 +1,135 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System; using UnityEngine.Events; // 유니티 이벤트 기능을 사용할거에요 -> UnityEngine.Events를
using System.Collections; using System; // C# 기본 이벤트(Action)를 사용할거에요 -> System을
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
public class PlayerHealth : MonoBehaviour, IDamageable /// <summary>
/// 플레이어의 체력, 피격, 무적, 사망을 관리합니다.
/// </summary>
public class PlayerHealth : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> PlayerHealth를
{ {
[Header("=== 참조 ===")] [Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 참조 === 를
[SerializeField] private Stats stats; [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 스탯 스크립트를 playerStats에
[SerializeField] private Animator animator;
[SerializeField] private PlayerAttack attackScript;
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [Header("=== 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 설정 === 을
// 🔗 보스 패턴 스크립트 연결 (Inspector에서 할당) [SerializeField] private float invincibleDuration = 1.0f; // 변수를 선언할거에요 -> 무적 시간을 invincibleDuration에
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [SerializeField] private string hitAnimationName = "Player_Hit"; // 변수를 선언할거에요 -> 피격 애니메이션 이름을 hitAnimationName에
[Header("🔗 보스 패턴 연결")]
public BossPatternPhases bossPattern;
public bool IsDead { get; private set; } [Header("=== 유니티 이벤트 (인스펙터 연결용) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 유니티 이벤트 === 를
public bool isHit { get; private set; } public UnityEvent OnDieEvent; // 이벤트를 선언할거에요 -> 사망 시 발생할 UnityEvent인 OnDieEvent를
public bool isInvincible; // 대시 중 무적 플래그 public UnityEvent<float> OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 시 발생할 OnHealthChanged를
public event Action OnHitEvent, OnDead; // ⭐ [이벤트] += 연산자 오류 방지를 위해 event Action 사용
public event Action<float, float> OnHealthChanged; 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; animator = GetComponentInChildren<Animator>(); // 컴포넌트를 가져올거에요 -> 자식의 애니메이터를
if (playerStats == null) playerStats = GetComponent<Stats>(); // 조건이 맞으면 가져올거에요 -> 스탯 컴포넌트가 비어있다면 내 몸에서
if (stats != null)
{
_currentHealth = stats.MaxHealth;
OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth);
Debug.Log($"<color=cyan>[UI Sync]</color> 초기 체력 설정 완료: {_currentHealth}/{stats.MaxHealth}");
}
if (animator == null) animator = GetComponent<Animator>();
if (attackScript == null) attackScript = GetComponent<PlayerAttack>();
} }
public void RefreshHealthUI() private void Start() // 함수를 실행할거에요 -> 시작 Start를
{ {
if (stats != null) if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 스탯이 있다면
{ {
_currentHealth = Mathf.Min(_currentHealth, stats.MaxHealth); CurrentHP = playerStats.MaxHealth; // 값을 초기화할거에요 -> 현재 체력을 최대 체력으로
OnHealthChanged?.Invoke(_currentHealth, stats.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();
} }
} }
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ private void Die() // 함수를 선언할거에요 -> 사망 처리 Die를
// ⭐ 데미지를 받는 함수
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public void TakeDamage(float amount)
{ {
// 무적이거나 이미 죽었으면 데미지 무시 if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
if (isInvincible || IsDead) return;
_currentHealth = Mathf.Max(0, _currentHealth - amount); IsDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참으로
OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ OnDieEvent?.Invoke(); // 유니티 이벤트
// 🔗 NEW: 보스 패턴 시스템에 "피격당함"을 알림 OnDie?.Invoke(); // C# 이벤트
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ OnDead?.Invoke(); // C# 이벤트 (호환용)
if (bossPattern != null)
{ Debug.Log("💀 플레이어 사망!"); // 로그를 출력할거에요 -> 사망 메시지를
// BossPatternPhases 스크립트의 OnPlayerHit() 함수 호출
// 이 함수 안에서 "패턴 진행 중인지" 체크하고 XP 감점 처리
bossPattern.OnPlayerHit();
} }
// ⭐ 피격 시 트리거 및 상태 리셋 함수 호출 public void RefreshHealthUI() // 함수를 선언할거에요 -> UI를 갱신하는 RefreshHealthUI를
OnHit(); {
float ratio = (playerStats != null && playerStats.MaxHealth > 0) ? CurrentHP / playerStats.MaxHealth : 0; // 비율을 계산할거에요 -> 현재 체력 / 최대 체력으로
OnHitEvent?.Invoke(); // 이벤트 발생 OnHealthChanged?.Invoke(ratio); // 이벤트를 실행할거에요 -> UI 슬라이더 값을 업데이트하도록
if (!IsDead) StartHit();
if (_currentHealth <= 0) Die();
} }
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ private IEnumerator HitRoutine() // 코루틴 함수를 선언할거에요 -> 피격 경직 및 무적 처리 HitRoutine을
// ⭐ 피격 시 공격 상태 리셋
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public void OnHit()
{ {
isHit = true; // 상태를 바꿀거에요 -> 경직 상태를 참으로
isInvincible = true; // 상태를 바꿀거에요 -> 무적 상태를 참으로
if (animator != null) if (animator != null)
{ {
// 1. 남아있는 공격 트리거를 강제로 꺼버림 (유령 공격 방지) animator.Play(hitAnimationName, 0, 0f); // 재생할거에요 -> 설정된 피격 애니메이션을
animator.ResetTrigger("Attack");
// 2. 다른 공격(예: 투척) 트리거가 있다면 그것도 리셋
animator.ResetTrigger("Throw");
} }
// 3. PlayerAttack 스크립트의 공격 중인 상태 플래그도 강제로 꺼줌 yield return new WaitForSeconds(0.3f); // 기다릴거에요 -> 경직 시간(0.3초)만큼
if (attackScript != null) isHit = false; // 상태를 바꿀거에요 -> 경직 해제
yield return new WaitForSeconds(invincibleDuration - 0.3f); // 기다릴거에요 -> 남은 무적 시간만큼
isInvincible = false; // 상태를 바꿀거에요 -> 무적 해제
}
public void Heal(float amount) // 함수를 선언할거에요 -> 체력을 회복하는 Heal을
{ {
attackScript.IsAttacking = false; if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 죽은 상태라면
}
Debug.Log("<color=yellow>[Combat]</color> 피격으로 인해 공격 예약 및 상태가 초기화되었습니다."); CurrentHP += amount; // 값을 더할거에요 -> 체력에 회복량을
} if (playerStats != null && CurrentHP > playerStats.MaxHealth) // 조건이 맞으면 실행할거에요 -> 최대 체력을 넘었다면
CurrentHP = playerStats.MaxHealth; // 값을 제한할거에요 -> 최대 체력으로
private void StartHit() RefreshHealthUI(); // 함수를 실행할거에요 -> UI 갱신을
{ Debug.Log($"💚 체력 회복: {amount}, 현재: {CurrentHP}"); // 로그를 출력할거에요 -> 회복 정보를
isHit = true;
// 인스펙터에 적힌 피격 애니메이션 이름(HitAnime) 재생
if (animator != null) animator.Play("HitAnime", 0, 0f);
CancelInvoke(nameof(OnHitEnd));
Invoke(nameof(OnHitEnd), 0.25f);
}
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);
} }
} }

View File

@ -1,14 +1,22 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
public class DamageBot : MonoBehaviour
{ public class DamageBot : MonoBehaviour // 클래스를 선언할거에요 -> 트리거 안에 있는 대상에게 주기적으로 데미지를 주는 DamageBot을
[SerializeField] private float damageAmount = 10f, damageInterval = 1.0f; { // 코드 블록을 시작할거에요 -> DamageBot 범위를
private float timer;
private void OnTriggerStay(Collider other) [SerializeField] private float damageAmount = 10f, damageInterval = 1.0f; // 변수를 선언할거에요 -> 데미지량(10)과 데미지 간격(1초)을
{ private float timer; // 변수를 선언할거에요 -> 시간 누적용 타이머를 timer에
if (other.TryGetComponent<IDamageable>(out var target))
{ private void OnTriggerStay(Collider other) // 함수를 선언할거에요 -> 트리거 안에 머무는 동안 계속 호출되는 OnTriggerStay를
timer += Time.deltaTime; { // 코드 블록을 시작할거에요 -> OnTriggerStay 범위를
if (timer >= damageInterval) { target.TakeDamage(damageAmount); timer = 0f; } if (other.TryGetComponent<IDamageable>(out var target)) // 조건을 검사할거에요 -> 대상이 IDamageable을 가지고 있는지
} { // 코드 블록을 시작할거에요 -> 데미지 가능 대상 처리
} timer += Time.deltaTime; // 값을 더할거에요 -> 지난 프레임 시간만큼 타이머 누적
} if (timer >= damageInterval) // 조건을 검사할거에요 -> 누적 시간이 간격을 넘었는지
{ // 코드 블록을 시작할거에요 -> 데미지 적용 처리
target.TakeDamage(damageAmount); // 함수를 실행할거에요 -> 대상에게 damageAmount만큼 데미지 주기
timer = 0f; // 값을 초기화할거에요 -> 다음 틱을 위해 타이머를 0으로
} // 코드 블록을 끝낼거에요 -> 데미지 적용 처리
} // 코드 블록을 끝낼거에요 -> 데미지 가능 대상 처리
} // 코드 블록을 끝낼거에요 -> OnTriggerStay를
} // 코드 블록을 끝낼거에요 -> DamageBot을

View File

@ -1,6 +1,8 @@
public interface IDamageable public interface IDamageable // 인터페이스를 선언할거에요 -> 데미지를 받을 수 있는 대상의 규칙(IDamageable)을
{ { // 코드 블록을 시작할거에요 -> IDamageable 범위를
// 공격을 받았을 때 실행될 함수입니다.
// 데미지 양(damage)을 인자로 받습니다. // 공격을 받았을 때 실행될 함수입니다. // 설명을 적을거에요 -> 데미지 받을 때 호출되는 함수임을
void TakeDamage(float damage); // 데미지 양(damage)을 인자로 받습니다. // 설명을 적을거에요 -> 들어오는 데미지 값이 damage라는 걸
} void TakeDamage(float damage); // 함수 원형을 정의할거에요 -> float damage를 받아 데미지 처리하는 TakeDamage를
} // 코드 블록을 끝낼거에요 -> IDamageable을

View File

@ -1,41 +1,43 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
using System.Collections.Generic; using System.Collections.Generic; // List를 쓰기 위해 불러올거에요 -> System.Collections.Generic을
public class WeaponHitBox : MonoBehaviour public class WeaponHitBox : MonoBehaviour // 클래스를 선언할거에요 -> 무기 히트박스(트리거)로 타격을 처리하는 WeaponHitBox를
{ { // 코드 블록을 시작할거에요 -> WeaponHitBox 범위를
private float _damage;
private bool _isActive = false;
private List<IDamageable> _hitTargets = new List<IDamageable>();
public void EnableHitBox(float damage) private float _damage; // 변수를 선언할거에요 -> 이번 공격에서 적용할 데미지를 _damage에
{ private bool _isActive = false; // 변수를 선언할거에요 -> 히트박스 활성 여부를 _isActive에(기본 false)
_damage = damage; private List<IDamageable> _hitTargets = new List<IDamageable>(); // 리스트를 만들거에요 -> 이미 때린 대상 저장용 _hitTargets를
_isActive = true;
_hitTargets.Clear();
gameObject.SetActive(true);
}
public void DisableHitBox() public void EnableHitBox(float damage) // 함수를 선언할거에요 -> 히트박스를 켜고 데미지를 세팅하는 EnableHitBox를
{ { // 코드 블록을 시작할거에요 -> EnableHitBox 범위를
_isActive = false; _damage = damage; // 값을 넣을거에요 -> 받은 damage를 _damage로 설정
gameObject.SetActive(false); _isActive = true; // 상태를 바꿀거에요 -> 히트박스를 활성 상태(true)로
} _hitTargets.Clear(); // 리스트를 비울거에요 -> 이전 공격에서 맞춘 기록을 초기화
gameObject.SetActive(true); // 오브젝트를 켤거에요 -> 히트박스 트리거 오브젝트 활성화
} // 코드 블록을 끝낼거에요 -> EnableHitBox를
private void OnTriggerEnter(Collider other) public void DisableHitBox() // 함수를 선언할거에요 -> 히트박스를 끄는 DisableHitBox를
{ { // 코드 블록을 시작할거에요 -> DisableHitBox 범위를
if (!_isActive || other.CompareTag("Player")) return; _isActive = false; // 상태를 바꿀거에요 -> 히트박스를 비활성(false)로
gameObject.SetActive(false); // 오브젝트를 끌거에요 -> 히트박스 트리거 오브젝트 비활성화
} // 코드 블록을 끝낼거에요 -> DisableHitBox를
// ⭐ [핵심] 몬스터의 감지 영역(Trigger)은 무시하고 '진짜 몸통'만 타격! private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 뭔가 들어오면 호출되는 OnTriggerEnter를
if (other.isTrigger) return; { // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를
if (!_isActive || other.CompareTag("Player")) return; // 조건이 맞으면 종료할거에요 -> 비활성이거나 플레이어면 무시
if (other.TryGetComponent<IDamageable>(out var target)) // ⭐ [핵심] 몬스터의 감지 영역(Trigger)은 무시하고 '진짜 몸통'만 타격! // 설명을 적을거에요 -> 트리거 콜라이더는 공격 대상으로 안 잡겠다는 뜻
{ if (other.isTrigger) return; // 조건이 맞으면 종료할거에요 -> 상대 콜라이더가 트리거면 무시
if (!_hitTargets.Contains(target))
{ if (other.TryGetComponent<IDamageable>(out var target)) // 조건을 검사할거에요 -> IDamageable을 가진 대상인지
target.TakeDamage(_damage); { // 코드 블록을 시작할거에요 -> 데미지 처리
_hitTargets.Add(target); if (!_hitTargets.Contains(target)) // 조건을 검사할거에요 -> 이번 공격에서 아직 안 때린 대상인지
Debug.Log($"<color=green>[Hit]</color> {other.name}의 몸통을 정확히 타격!"); { // 코드 블록을 시작할거에요 -> 1회 타격 처리
} target.TakeDamage(_damage); // 함수를 실행할거에요 -> 대상에게 _damage만큼 데미지 주기
} _hitTargets.Add(target); // 리스트에 추가할거에요 -> 중복 타격 방지를 위해 대상 기록
} Debug.Log($"<color=green>[Hit]</color> {other.name}의 몸통을 정확히 타격!"); // 로그를 찍을거에요 -> 타격 확인
} } // 코드 블록을 끝낼거에요 -> 1회 타격 처리
} // 코드 블록을 끝낼거에요 -> 데미지 처리
} // 코드 블록을 끝낼거에요 -> OnTriggerEnter를
} // 코드 블록을 끝낼거에요 -> WeaponHitBox를

View File

@ -1,39 +1,41 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능 + ScriptableObject를 쓰기 위해 불러올거에요 -> UnityEngine를
[CreateAssetMenu(fileName = "NewWeapon", menuName = "Weapons/Config")] [CreateAssetMenu(fileName = "NewWeapon", menuName = "Weapons/Config")] // 에셋 생성 메뉴를 추가할거에요 -> Create > Weapons > Config로 WeaponConfig를 만들 수 있게
public class WeaponConfig : ScriptableObject public class WeaponConfig : ScriptableObject // 클래스를 선언할거에요 -> 무기 설정 데이터를 담는 ScriptableObject인 WeaponConfig를
{ { // 코드 블록을 시작할거에요 -> WeaponConfig 범위를
[Header("기본 능력치")]
[SerializeField] private float baseDamage = 10f;
[SerializeField] private int requiredStrength = 5;
public float BaseDamage => baseDamage; [Header("기본 능력치")] // 인스펙터에 제목을 표시할거에요 -> 기본 능력치 섹션을
public int RequiredStrength => requiredStrength; [SerializeField] private float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 데미지를 10으로 baseDamage에
[SerializeField] private int requiredStrength = 5; // 변수를 선언할거에요 -> 요구 힘 스탯을 5로 requiredStrength에
[Header("애니메이션 설정")] public float BaseDamage => baseDamage; // 프로퍼티를 선언할거에요 -> baseDamage를 읽기 전용으로 노출
[SerializeField] private string normalAttackTrigger = "Attack"; public int RequiredStrength => requiredStrength; // 프로퍼티를 선언할거에요 -> requiredStrength를 읽기 전용으로 노출
[SerializeField] private string chargingBool = "IsCharging";
[SerializeField] private string throwTrigger = "Throw";
public string NormalAttackTrigger => normalAttackTrigger; [Header("애니메이션 설정")] // 인스펙터에 제목을 표시할거에요 -> 애니메이션 설정 섹션을
public string ChargingBool => chargingBool; [SerializeField] private string normalAttackTrigger = "Attack"; // 변수를 선언할거에요 -> 일반 공격 트리거 이름을 normalAttackTrigger에
public string ThrowTrigger => throwTrigger; [SerializeField] private string chargingBool = "IsCharging"; // 변수를 선언할거에요 -> 차징 여부 bool 파라미터 이름을 chargingBool에
[SerializeField] private string throwTrigger = "Throw"; // 변수를 선언할거에요 -> 던지기 트리거 이름을 throwTrigger에
[Header("차징 단계별 설정 (인스펙터 수정)")] public string NormalAttackTrigger => normalAttackTrigger; // 프로퍼티를 선언할거에요 -> 일반 공격 트리거 문자열을 읽기 전용으로 노출
[SerializeField] private float forceLv1 = 10f; public string ChargingBool => chargingBool; // 프로퍼티를 선언할거에요 -> 차징 bool 문자열을 읽기 전용으로 노출
[SerializeField] private float spreadLv1 = 25f; public string ThrowTrigger => throwTrigger; // 프로퍼티를 선언할거에요 -> 던지기 트리거 문자열을 읽기 전용으로 노출
[SerializeField] private float forceLv2 = 18f;
[SerializeField] private float spreadLv2 = 8f;
[SerializeField] private float forceLv3 = 28f;
// Lv3 Spread는 인스펙터에서 보이지 않게 하거나, 로직에서 무시하도록 처리합니다.
// ⭐ 힘(Force)은 단계별로 가져옵니다. [Header("차징 단계별 설정 (인스펙터 수정)")] // 인스펙터에 제목을 표시할거에요 -> 차징 단계별 설정 섹션을
public float GetForce(int lv) => lv == 3 ? forceLv3 : (lv == 2 ? forceLv2 : forceLv1); [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(직선) 반환! // ⭐ 힘(Force)은 단계별로 가져옵니다. // 설명을 적을거에요 -> 단계에 맞는 force를 반환함을
public float GetSpread(int lv) public float GetForce(int lv) => lv == 3 ? forceLv3 : (lv == 2 ? forceLv2 : forceLv1); // 값을 반환할거에요 -> lv에 따라 forceLv1/2/3 중 하나를
{
if (lv == 3) return 0f; // 풀차징은 무조건 일자로 날아감 // ⭐ [수정] 정확도(Spread) 로직: 3단계는 무조건 0(직선) 반환! // 설명을 적을거에요 -> 풀차징은 직선 고정임을
return (lv == 2) ? spreadLv2 : spreadLv1; 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를

View File

@ -1,211 +1,211 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.AI; using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
using System.Collections.Generic; // ✅ 리스트 사용 필수 using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
/// <summary> /// <summary>
/// 돌진 공격 몬스터 /// 돌진 공격 몬스터
/// </summary> /// </summary>
public class ChargeMonster : MonsterClass public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ChargeMonster를
{ {
[Header("=== 돌진 공격 설정 ===")] [Header("=== 돌진 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 돌진 공격 설정 === 을
[SerializeField] private float chargeSpeed = 15f; [SerializeField] private float chargeSpeed = 15f; // 변수를 선언할거에요 -> 돌진 속도(15.0)를 chargeSpeed에
[SerializeField] private float chargeRange = 10f; [SerializeField] private float chargeRange = 10f; // 변수를 선언할거에요 -> 돌진 시작 거리(10.0)를 chargeRange에
[SerializeField] private float chargeDuration = 2f; [SerializeField] private float chargeDuration = 2f; // 변수를 선언할거에요 -> 돌진 지속 시간(2.0초)을 chargeDuration에
[SerializeField] private float chargeDelay = 3f; [SerializeField] private float chargeDelay = 3f; // 변수를 선언할거에요 -> 돌진 쿨타임(3.0초)을 chargeDelay에
[SerializeField] private float prepareTime = 0.5f; [SerializeField] private float prepareTime = 0.5f; // 변수를 선언할거에요 -> 돌진 준비 시간(0.5초)을 prepareTime에
// 🎁 [추가] 드랍 아이템 리스트 // 🎁 [추가] 드랍 아이템 리스트
[Header("=== 드랍 아이템 설정 ===")] [Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을
[Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")] [Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private List<GameObject> dropItemPrefabs; [SerializeField] private List<GameObject> dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍 아이템 프리팹 목록을 dropItemPrefabs에
[Tooltip("아이템이 나올 확률 (0 ~ 100%)")] [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[Range(0, 100)][SerializeField] private float dropChance = 30f; [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에
private float lastChargeTime; private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을 lastChargeTime에
private bool isCharging = false; private bool isCharging = false; // 변수를 선언할거에요 -> 돌진 중 여부를 isCharging에
private bool isPreparing = false; private bool isPreparing = false; // 변수를 선언할거에요 -> 준비 중 여부를 isPreparing에
private Vector3 chargeDirection; private Vector3 chargeDirection; // 변수를 선언할거에요 -> 돌진 방향 벡터를 chargeDirection에
[Header("공격 / 이동 애니메이션")] [Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을
[SerializeField] private string chargeAnimation = "Monster_Charge"; [SerializeField] private string chargeAnimation = "Monster_Charge"; // 변수를 선언할거에요 -> 돌진 애니메이션 이름을 chargeAnimation에
[SerializeField] private string prepareAnimation = "Monster_ChargePrepare"; [SerializeField] private string prepareAnimation = "Monster_ChargePrepare"; // 변수를 선언할거에요 -> 준비 애니메이션 이름을 prepareAnimation에
[SerializeField] private string Monster_Walk = "Monster_Walk"; [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에
[Header("AI 상세 설정")] [Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을
[SerializeField] private float patrolRadius = 5f; [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
[SerializeField] private float patrolInterval = 2f; [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에
private float nextPatrolTime; private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
private bool isPlayerInZone; private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에
private Rigidbody _rigidbody; private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트를 담을 _rigidbody를
protected override void Awake() protected override void Awake() // 함수를 덮어씌워 실행할거에요 -> Awake 초기화 로직을
{ {
base.Awake(); base.Awake(); // 부모의 함수를 실행할거에요 -> MonsterClass의 Awake를
_rigidbody = GetComponent<Rigidbody>(); _rigidbody = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> Rigidbody를
if (_rigidbody == null) _rigidbody = gameObject.AddComponent<Rigidbody>(); if (_rigidbody == null) _rigidbody = gameObject.AddComponent<Rigidbody>(); // 조건이 맞으면 실행할거에요 -> 없으면 새로 추가해서 _rigidbody에
_rigidbody.isKinematic = true; _rigidbody.isKinematic = true; // 설정을 바꿀거에요 -> 물리 연산을 꺼두기(Kinematic)로
} }
protected override void Init() protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기 설정을 Init에서
{ {
if (agent != null) agent.stoppingDistance = 1f; if (agent != null) agent.stoppingDistance = 1f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 1.0으로
if (animator != null) animator.applyRootMotion = false; 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; if (agent.isStopped) agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고
agent.SetDestination(playerTransform.position); agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
} }
} }
} }
IEnumerator PrepareCharge() IEnumerator PrepareCharge() // 코루틴 함수를 선언할거에요 -> 돌진 준비 과정인 PrepareCharge를
{ {
isPreparing = true; isPreparing = true; // 상태를 바꿀거에요 -> 준비 중 상태를 참(true)으로
if (agent.isOnNavMesh) if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
{ {
agent.isStopped = true; agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
agent.ResetPath(); agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 초기화하라고
agent.velocity = Vector3.zero; agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
} }
chargeDirection = (playerTransform.position - transform.position).normalized; chargeDirection = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을
chargeDirection.y = 0; chargeDirection.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게 y를 0으로
if (chargeDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(chargeDirection); 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] 돌진 준비..."); Debug.Log("[ChargeMonster] 돌진 준비..."); // 로그를 출력할거에요 -> 준비 메시지를
yield return new WaitForSeconds(prepareTime); yield return new WaitForSeconds(prepareTime); // 기다릴거에요 -> 준비 시간만큼
StartCoroutine(Charge()); StartCoroutine(Charge()); // 코루틴을 실행할거에요 -> 실제 돌진인 Charge를
} }
IEnumerator Charge() IEnumerator Charge() // 코루틴 함수를 선언할거에요 -> 실제 돌진 동작인 Charge를
{ {
isPreparing = false; isPreparing = false; // 상태를 바꿀거에요 -> 준비 상태를 거짓(false)으로
isCharging = true; isCharging = true; // 상태를 바꿀거에요 -> 돌진 중 상태를 참(true)으로
lastChargeTime = Time.time; lastChargeTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 돌진 시간으로
if (agent != null) agent.enabled = false; if (agent != null) agent.enabled = false; // 기능을 끌거에요 -> 길찾기 에이전트를 (물리 이동을 위해)
if (_rigidbody != null) _rigidbody.isKinematic = false; if (_rigidbody != null) _rigidbody.isKinematic = false; // 기능을 켤거에요 -> 물리 연산을 (충돌 감지를 위해)
animator.Play(chargeAnimation, 0, 0f); animator.Play(chargeAnimation, 0, 0f); // 재생할거에요 -> 돌진 애니메이션을
Debug.Log("[ChargeMonster] 돌진 시작!"); Debug.Log("[ChargeMonster] 돌진 시작!"); // 로그를 출력할거에요 -> 돌진 시작 메시지를
float chargeStartTime = Time.time; float chargeStartTime = Time.time; // 값을 저장할거에요 -> 돌진 시작 시간을
while (Time.time < chargeStartTime + chargeDuration) while (Time.time < chargeStartTime + chargeDuration) // 반복할거에요 -> 지속 시간이 끝날 때까지
{ {
if (_rigidbody != null) if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
_rigidbody.velocity = chargeDirection * chargeSpeed; _rigidbody.velocity = chargeDirection * chargeSpeed; // 값을 넣을거에요 -> 속도를 돌진 방향과 속도로
else else // 조건이 틀리면 실행할거에요 -> 리지드바디가 없다면 (비상용)
transform.position += chargeDirection * chargeSpeed * Time.deltaTime; transform.position += chargeDirection * chargeSpeed * Time.deltaTime; // 이동시킬거에요 -> 위치를 직접 수정해서
yield return null; yield return null; // 대기할거에요 -> 다음 프레임까지
} }
if (_rigidbody != null) if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
{ {
_rigidbody.velocity = Vector3.zero; _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
_rigidbody.isKinematic = true; _rigidbody.isKinematic = true; // 기능을 끌거에요 -> 물리 연산을 (다시 NavMesh 이동을 위해)
} }
if (agent != null) if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
{ {
agent.enabled = true; agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를
agent.ResetPath(); agent.ResetPath(); // 초기화할거에요 -> 경로를
agent.velocity = Vector3.zero; agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 내부 속도를 0으로
} }
isCharging = false; isCharging = false; // 상태를 바꿀거에요 -> 돌진 중 상태를 거짓(false)으로
Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작"); Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작"); // 로그를 출력할거에요 -> 종료 메시지를
} }
void UpdateMovementAnimation() void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을
{ {
if (isCharging || isPreparing || isHit || isResting) return; if (isCharging || isPreparing || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
if (agent.enabled && agent.velocity.magnitude > 0.1f) if (agent.enabled && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
animator.Play(Monster_Walk); animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을
else else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
animator.Play(Monster_Idle); 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), Random.Range(-patrolRadius, patrolRadius),
0, 0,
Random.Range(-patrolRadius, patrolRadius) Random.Range(-patrolRadius, patrolRadius)
); );
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 NavMesh 위치라면
if (agent.isOnNavMesh) agent.SetDestination(hit.position); 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 (!isCharging) return; // 조건이 맞으면 중단할거에요 -> 돌진 중이 아니라면
if (collision.gameObject.CompareTag("Player")) if (collision.gameObject.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌한 대상이 플레이어라면
{ {
if (collision.gameObject.TryGetComponent<PlayerHealth>(out var playerHealth)) if (collision.gameObject.TryGetComponent<PlayerHealth>(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를
{ {
if (!playerHealth.isInvincible) if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면
playerHealth.TakeDamage(attackDamage); 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); float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0~100 사이 랜덤값을
if (randomValue <= dropChance) if (randomValue <= dropChance) // 조건이 맞으면 실행할거에요 -> 확률에 당첨되었다면
{ {
int randomIndex = Random.Range(0, dropItemPrefabs.Count); int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 랜덤 인덱스를
if (dropItemPrefabs[randomIndex] != null) 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 OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 감지 시 참(true)으로
private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어가 나가면 거짓(false)으로
} }

View File

@ -1,211 +1,211 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.AI; using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
/// <summary> /// <summary>
/// 자폭 몬스터 (Kamikaze) /// 자폭 몬스터 (Kamikaze)
/// - 플레이어에게 전력 질주 /// - 플레이어에게 전력 질주
/// - 범위 내 진입 시 제자리에서 카운트다운 후 폭발 /// - 범위 내 진입 시 제자리에서 카운트다운 후 폭발
/// </summary> /// </summary>
public class ExplodeMonster : MonsterClass public class ExplodeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ExplodeMonster를
{ {
[Header("=== 자폭 설정 ===")] [Header("=== 자폭 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 자폭 설정 === 을
[SerializeField] private float explodeRange = 4f; // 데미지 입히는 범위 (폭발 반경) [SerializeField] private float explodeRange = 4f; // 변수를 선언할거에요 -> 폭발 데미지 반경(4.0)을 explodeRange에
[SerializeField] private float triggerRange = 2.5f; // 폭발을 시작하는 거리 (이 안에 들어오면 카운트다운) [SerializeField] private float triggerRange = 2.5f; // 변수를 선언할거에요 -> 자폭 시퀀스 시작 거리(2.5)를 triggerRange에
[SerializeField] private float fuseTime = 1.5f; // 지연 시간 (도망갈 기회 줌) [SerializeField] private float fuseTime = 1.5f; // 변수를 선언할거에요 -> 폭발 지연 시간(1.5초)을 fuseTime에
[SerializeField] private float explosionDamage = 50f; // 폭발 데미지 [SerializeField] private float explosionDamage = 50f; // 변수를 선언할거에요 -> 폭발 데미지(50.0)를 explosionDamage에
[Header("폭발 효과")] [Header("폭발 효과")] // 인스펙터 창에 제목을 표시할거에요 -> 폭발 효과 를
[SerializeField] private GameObject explosionEffectPrefab; // 쾅! 이펙트 프리팹 [SerializeField] private GameObject explosionEffectPrefab; // 변수를 선언할거에요 -> 폭발 이펙트 프리팹을 explosionEffectPrefab에
[SerializeField] private ParticleSystem fuseEffect; // 몸에서 불꽃 튀는 이펙트 (선택) [SerializeField] private ParticleSystem fuseEffect; // 변수를 선언할거에요 -> 도화선(준비) 이펙트를 fuseEffect에
[SerializeField] private AudioClip fuseSound; // 치익~ 소리 [SerializeField] private AudioClip fuseSound; // 변수를 선언할거에요 -> 도화선 소리를 fuseSound에
[SerializeField] private AudioClip explosionSound; // 쾅! 소리 [SerializeField] private AudioClip explosionSound; // 변수를 선언할거에요 -> 폭발 소리를 explosionSound에
[Header("애니메이션")] [Header("애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 을
[SerializeField] private string runAnimation = "Monster_Run"; // 달리기 [SerializeField] private string runAnimation = "Monster_Run"; // 변수를 선언할거에요 -> 달리기 애니메이션 이름을 runAnimation에
[SerializeField] private string fuseAnimation = "Monster_Fuse"; // 부들부들(폭발 준비) [SerializeField] private string fuseAnimation = "Monster_Fuse"; // 변수를 선언할거에요 -> 자폭 준비 애니메이션 이름을 fuseAnimation에
[Header("AI 설정")] [Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을
[SerializeField] private float chaseSpeed = 6f; // 이동 속도 (다른 애들보다 빨라야 무서움) [SerializeField] private float chaseSpeed = 6f; // 변수를 선언할거에요 -> 추격 속도(6.0)를 chaseSpeed에
[SerializeField] private float patrolRadius = 5f; [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
[SerializeField] private float patrolInterval = 2f; [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에
private bool isExploding = false; // 폭발 진행 중? private bool isExploding = false; // 변수를 초기화할거에요 -> 폭발 진행 중 여부를 거짓(false)으로
private bool hasExploded = false; // 이미 터졌나? private bool hasExploded = false; // 변수를 초기화할거에요 -> 이미 터졌는지 여부를 거짓(false)으로
private float nextPatrolTime; private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
private bool isPlayerInZone; private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 초기화 // 초기화
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
protected override void Init() protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
{ {
if (agent != null) if (agent != null)
{ {
agent.speed = chaseSpeed; // 속도 설정 agent.speed = chaseSpeed; // 값을 설정할거에요 -> 이동 속도를 추격 속도로
agent.stoppingDistance = 0.5f; // 바짝 붙음 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; isExploding = false; // 상태를 바꿀거에요 -> 폭발 진행 상태를 거짓(false)으로
hasExploded = false; hasExploded = false; // 상태를 바꿀거에요 -> 폭발 완료 상태를 거짓(false)으로
if (fuseEffect != null) fuseEffect.Stop(); if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트를 끄기를
} }
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// AI 로직 // 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) 안에 있거나 트리거에 닿았으면 추격 // 플레이어가 감지 범위(15m) 안에 있거나 트리거에 닿았으면 추격
if (isPlayerInZone || distance <= 15f) if (isPlayerInZone || distance <= 15f) // 조건이 맞으면 실행할거에요 -> 추격 조건이 만족되면
{ {
ChasePlayer(distance); ChasePlayer(distance); // 함수를 실행할거에요 -> 추격 및 자폭 처리를 하는 ChasePlayer를
} }
else else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
{ {
Patrol(); Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
UpdateMovementAnimation(); UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션을 갱신하는 UpdateMovementAnimation을
} }
} }
void ChasePlayer(float distance) void ChasePlayer(float distance) // 함수를 선언할거에요 -> 거리별 추격 행동을 정하는 ChasePlayer를
{ {
// 1. 폭발 시작 거리 안으로 들어왔다? -> 점화! // 1. 폭발 시작 거리 안으로 들어왔다? -> 점화!
if (distance <= triggerRange) if (distance <= triggerRange) // 조건이 맞으면 실행할거에요 -> 자폭 거리 이내라면
{ {
StartCoroutine(ExplodeRoutine()); StartCoroutine(ExplodeRoutine()); // 코루틴을 실행할거에요 -> 자폭 시퀀스인 ExplodeRoutine을
return; return; // 중단할거에요 -> 더 이상 이동하지 않도록
} }
// 2. 아직 멀었다? -> 전력 질주 // 2. 아직 멀었다? -> 전력 질주
if (agent.isOnNavMesh) if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
{ {
agent.isStopped = false; agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고
agent.SetDestination(playerTransform.position); 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) if (agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
animator.Play(runAnimation); animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을
else else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
animator.Play(Monster_Idle); 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), Random.Range(-patrolRadius, patrolRadius),
0, 0,
Random.Range(-patrolRadius, patrolRadius) Random.Range(-patrolRadius, patrolRadius)
); );
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 위치라면
if (agent.isOnNavMesh) agent.SetDestination(hit.position); 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; isExploding = true; // 상태를 바꿀거에요 -> 폭발 진행 상태를 참(true)으로
hasExploded = true; hasExploded = true; // 상태를 바꿀거에요 -> 폭발 완료 상태를 참(true)으로 (중복 방지)
isAttacking = true; // 부모 클래스 간섭 방지 isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 (부모 간섭 방지)
// ⭐ [핵심] 급브레이크! (문워크 방지) // ⭐ [핵심] 급브레이크! (문워크 방지)
if (agent.isOnNavMesh) if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
{ {
agent.isStopped = true; agent.isStopped = true; // 명령을 내릴거에요 -> 멈추라고
agent.ResetPath(); // 목적지 삭제 agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 지우라고
agent.velocity = Vector3.zero; // 속도 0 agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도 0으로
} }
Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)"); Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)"); // 로그를 출력할거에요 -> 자폭 예고 메시지를
// 1. 부들부들 떨기 (폭발 준비 모션) // 1. 부들부들 떨기 (폭발 준비 모션)
if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation); if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation); // 재생할거에요 -> 준비 애니메이션을
// 2. 치익~ 소리 & 이펙트 // 2. 치익~ 소리 & 이펙트
if (fuseEffect != null) fuseEffect.Play(); if (fuseEffect != null) fuseEffect.Play(); // 실행할거에요 -> 도화선 이펙트를
if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound); if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound); // 재생할거에요 -> 도화선 소리를
// 3. 도망갈 시간 주기 // 3. 도망갈 시간 주기
yield return new WaitForSeconds(fuseTime); yield return new WaitForSeconds(fuseTime); // 기다릴거에요 -> 폭발 지연 시간만큼
// 4. 쾅! // 4. 쾅!
PerformExplosion(); PerformExplosion(); // 함수를 실행할거에요 -> 실제 폭발 처리를
} }
void PerformExplosion() void PerformExplosion() // 함수를 선언할거에요 -> 폭발 데미지와 이펙트를 처리하는 PerformExplosion을
{ {
Debug.Log("💥💥💥 쾅!!!"); Debug.Log("💥💥💥 쾅!!!"); // 로그를 출력할거에요 -> 폭발 메시지를
// 폭발 이펙트 생성 (Particle System) // 폭발 이펙트 생성 (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 기능 사용) // 몬스터 사망 처리 (MonsterClass 기능 사용)
Die(); Die(); // 함수를 실행할거에요 -> 몬스터를 죽게 만드는 Die를
} }
void DamageNearbyTargets() void DamageNearbyTargets() // 함수를 선언할거에요 -> 폭발 범위 내 데미지를 입히는 DamageNearbyTargets를
{ {
// 폭발 범위(Sphere) 안에 있는 모든 물체 검사 // 폭발 범위(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<PlayerHealth>(out var playerHealth)) if (hit.TryGetComponent<PlayerHealth>(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를
{ {
if (!playerHealth.isInvincible) if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면
{ {
playerHealth.TakeDamage(explosionDamage); playerHealth.TakeDamage(explosionDamage); // 함수를 실행할거에요 -> 폭발 데미지를 입히는 TakeDamage를
Debug.Log($"💥 플레이어 폭사! 데미지: {explosionDamage}"); 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 OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 진입 시 감지 상태를 참으로
private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어 이탈 시 감지 상태를 거짓으로
// 에디터에서 범위 보여주기 // 에디터에서 범위 보여주기
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 범위를 그리는 OnDrawGizmosSelected를
{ {
Gizmos.color = Color.red; Gizmos.color = Color.red; // 색상을 설정할거에요 -> 감지 범위 색을 빨간색으로
Gizmos.DrawWireSphere(transform.position, triggerRange); // 감지 범위 Gizmos.DrawWireSphere(transform.position, triggerRange); // 그림을 그릴거에요 -> 자폭 감지 범위
Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); // 색상을 설정할거에요 -> 폭발 범위 색을 주황색 반투명으로
Gizmos.DrawSphere(transform.position, explodeRange); // 폭발 데미지 범위 Gizmos.DrawSphere(transform.position, explodeRange); // 그림을 그릴거에요 -> 폭발 데미지 범위
} }
} }

View File

@ -1,77 +1,80 @@
using System.Collections.Generic; using System.Collections.Generic; // 제네릭 컬렉션을 쓰기 위해 불러올거에요 -> System.Collections.Generic을
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
public class MonsterWeapon : MonoBehaviour public class MonsterWeapon : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterWeapon을
{ { // 코드 블록을 시작할거에요 -> MonsterWeapon 범위를
[Header("무기 설정")]
[Tooltip("이 무기 고유의 공격력 (예: 10)")]
[SerializeField] private float weaponBaseDamage = 10f;
private float _finalDamage; [Header("무기 설정")] // 인스펙터에 제목을 표시할거에요 -> 무기 설정을
[SerializeField] private BoxCollider _weaponCollider; [Tooltip("이 무기 고유의 공격력 (예: 10)")] // 인스펙터에 툴팁을 달거에요 -> weaponBaseDamage 설명을
[SerializeField] private float weaponBaseDamage = 10f; // 변수를 선언할거에요 -> 무기 기본 공격력을 10으로
private void Awake() private float _finalDamage; // 변수를 선언할거에요 -> 최종 데미지를 저장할 _finalDamage를
{ [SerializeField] private BoxCollider _weaponCollider; // 변수를 선언할거에요 -> 무기 콜라이더를 저장할 _weaponCollider를
_weaponCollider = GetComponent<BoxCollider>();
_finalDamage = weaponBaseDamage;
//DisableHitBox();
EnableHitBox();
}
public void SetDamage(float monsterStrength) private void Awake() // 함수를 선언할거에요 -> 시작 시 1번 실행되는 Awake를
{ { // 코드 블록을 시작할거에요 -> Awake 범위를
_finalDamage = weaponBaseDamage + monsterStrength; _weaponCollider = GetComponent<BoxCollider>(); // 컴포넌트를 가져올거에요 -> 내 오브젝트의 BoxCollider를 찾아 _weaponCollider에
Debug.Log($"✅ [무기] 데미지 설정됨: 총 {_finalDamage}"); _finalDamage = weaponBaseDamage; // 값을 넣을거에요 -> 최종 데미지를 기본 데미지로 초기화
} //DisableHitBox(); // 주석 처리할거에요 -> 시작 시 판정을 끄는 코드(현재는 안 씀)
EnableHitBox(); // 함수를 실행할거에요 -> 시작하자마자 판정을 켜기(현재 설정)
} // 코드 블록을 끝낼거에요 -> Awake를
public void EnableHitBox() public void SetDamage(float monsterStrength) // 함수를 선언할거에요 -> 몬스터 스탯을 받아 최종 데미지를 세팅하는 SetDamage를
{ { // 코드 블록을 시작할거에요 -> SetDamage 범위를
Debug.Log("enabletest"); _finalDamage = weaponBaseDamage + monsterStrength; // 값을 계산할거에요 -> 기본 데미지 + 몬스터 힘으로 최종 데미지 설정
Debug.Log($"✅ [무기] 데미지 설정됨: 총 {_finalDamage}"); // 로그를 찍을거에요 -> 최종 데미지가 얼마인지
} // 코드 블록을 끝낼거에요 -> SetDamage를
if (_weaponCollider != null) public void EnableHitBox() // 함수를 선언할거에요 -> 공격 판정을 켜는 EnableHitBox를
{ { // 코드 블록을 시작할거에요 -> EnableHitBox 범위를
Debug.Log("setcollider"); Debug.Log("enabletest"); // 로그를 찍을거에요 -> 함수가 호출됐는지 확인용
_weaponCollider.enabled = true;
Debug.Log("🔴 [무기] 공격 판정 ON! (휘두르기 시작)"); // 로그 추가
}
}
public void DisableHitBox() if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 정상적으로 잡혔는지
{ { // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리
if (_weaponCollider != null) Debug.Log("setcollider"); // 로그를 찍을거에요 -> 콜라이더 enable 직전 확인용
{ _weaponCollider.enabled = true; // 값을 바꿀거에요 -> 트리거 판정을 켜기
_weaponCollider.enabled = false; Debug.Log("🔴 [무기] 공격 판정 ON! (휘두르기 시작)"); // 로그를 찍을거에요 -> 판정이 켜졌음을
Debug.Log("⚪ [무기] 공격 판정 OFF (휘두르기 끝)"); // 로그 추가 } // 코드 블록을 끝낼거에요 -> 콜라이더 처리
} } // 코드 블록을 끝낼거에요 -> EnableHitBox를
}
// ⭐ 여기가 핵심! 닿는 모든 것을 기록함 public void DisableHitBox() // 함수를 선언할거에요 -> 공격 판정을 끄는 DisableHitBox를
private void OnTriggerEnter(Collider other) { // 코드 블록을 시작할거에요 -> DisableHitBox 범위를
{ if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 있는지
// 1. 무엇이든 닿으면 일단 로그를 찍음 (범인 색출) { // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리
Debug.Log($"💥 [충돌 감지] 무기가 닿은 것: {other.name} / 태그: {other.tag}"); _weaponCollider.enabled = false; // 값을 바꿀거에요 -> 트리거 판정을 끄기
Debug.Log("⚪ [무기] 공격 판정 OFF (휘두르기 끝)"); // 로그를 찍을거에요 -> 판정이 꺼졌음을
} // 코드 블록을 끝낼거에요 -> 콜라이더 처리
} // 코드 블록을 끝낼거에요 -> DisableHitBox를
if (other.CompareTag("Player")) // ⭐ 여기가 핵심! 닿는 모든 것을 기록함 // 설명을 적을거에요 -> 트리거 충돌 감지 핵심 구간임을
{ private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 다른 콜라이더가 들어오면 호출되는 OnTriggerEnter를
PlayerHealth playerHealth = other.GetComponent<PlayerHealth>(); { // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를
// 1. 무엇이든 닿으면 일단 로그를 찍음 (범인 색출) // 설명을 적을거에요 -> 뭐랑 부딪히는지 디버깅용
Debug.Log($"💥 [충돌 감지] 무기가 닿은 것: {other.name} / 태그: {other.tag}"); // 로그를 찍을거에요 -> 충돌한 오브젝트 이름/태그를
if (playerHealth != null) if (other.CompareTag("Player")) // 조건을 검사할거에요 -> 닿은 대상이 Player 태그인지
{ { // 코드 블록을 시작할거에요 -> 플레이어일 때 처리
if (!playerHealth.isInvincible) PlayerHealth playerHealth = other.GetComponent<PlayerHealth>(); // 컴포넌트를 가져올거에요 -> 플레이어의 PlayerHealth를 찾아 playerHealth에
{
playerHealth.TakeDamage(_finalDamage); if (playerHealth != null) // 조건을 검사할거에요 -> PlayerHealth가 실제로 붙어있는지
Debug.Log($"⚔️ [적중] 플레이어 체력 깎임! (-{_finalDamage})"); { // 코드 블록을 시작할거에요 -> PlayerHealth가 있을 때 처리
} if (!playerHealth.isInvincible) // 조건을 검사할거에요 -> 플레이어가 무적인지 아닌지
else { // 코드 블록을 시작할거에요 -> 무적이 아닐 때 처리
{ playerHealth.TakeDamage(_finalDamage); // 함수를 실행할거에요 -> 플레이어에게 최종 데미지 적용
Debug.Log("🛡️ [방어] 플레이어가 무적 상태입니다."); Debug.Log($"⚔️ [적중] 플레이어 체력 깎임! (-{_finalDamage})"); // 로그를 찍을거에요 -> 데미지가 들어갔음을
} } // 코드 블록을 끝낼거에요 -> 무적 아닐 때 처리
DisableHitBox(); else // 조건이 아니면 실행할거에요 -> 무적이면
} { // 코드 블록을 시작할거에요 -> 무적일 때 처리
else Debug.Log("🛡️ [방어] 플레이어가 무적 상태입니다."); // 로그를 찍을거에요 -> 무적이라 데미지 안 들어감을
{ } // 코드 블록을 끝낼거에요 -> 무적 처리
Debug.LogError("⚠️ [오류] 플레이어 태그는 있는데 'PlayerHealth' 스크립트가 없습니다!");
} DisableHitBox(); // 함수를 실행할거에요 -> 한 번 맞추면 바로 판정 끄기(중복 타격 방지)
} } // 코드 블록을 끝낼거에요 -> PlayerHealth 있을 때 처리
} else // 조건이 아니면 실행할거에요 -> PlayerHealth가 없으면
} { // 코드 블록을 시작할거에요 -> 오류 처리
Debug.LogError("⚠️ [오류] 플레이어 태그는 있는데 'PlayerHealth' 스크립트가 없습니다!"); // 에러 로그를 찍을거에요 -> 컴포넌트 누락 경고
} // 코드 블록을 끝낼거에요 -> 오류 처리
} // 코드 블록을 끝낼거에요 -> 플레이어 처리
} // 코드 블록을 끝낼거에요 -> OnTriggerEnter를
} // 코드 블록을 끝낼거에요 -> MonsterWeapon을

View File

@ -1,155 +1,163 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.AI; using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
using System; using System; // 기본 시스템 기능(Action 등)을 사용할거에요 -> System을
/// <summary> /// <summary>
/// 몬스터 기본 클래스 (공통 기능만) /// 몬스터 기본 클래스 (공통 기능만)
/// - 공격 방식은 자식 클래스에서 구현 /// - 공격 방식은 자식 클래스에서 구현
/// </summary> /// </summary>
public abstract class MonsterClass : MonoBehaviour, IDamageable public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 MonsterClass를
{ {
[Header("--- 최적화 설정 ---")] [Header("--- 최적화 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최적화 설정 --- 을
protected Renderer mobRenderer; protected Renderer mobRenderer; // 변수를 선언할거에요 -> 몬스터의 렌더러(외형) 컴포넌트를 담을 mobRenderer를
protected Transform playerTransform; protected Transform playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 playerTransform을
[SerializeField] protected float optimizationDistance = 40f; [SerializeField] protected float optimizationDistance = 40f; // 변수를 선언할거에요 -> 몬스터 최적화(AI 정지) 거리(40.0)를 optimizationDistance에
[Header("몬스터 기본 스탯")] [Header("몬스터 기본 스탯")] // 인스펙터 창에 제목을 표시할거에요 -> 몬스터 기본 스탯 을
[SerializeField] protected float maxHP = 100f; [SerializeField] protected float maxHP = 100f; // 변수를 선언할거에요 -> 최대 체력(100.0)을 maxHP에
[SerializeField] protected float attackDamage = 10f; [SerializeField] protected float attackDamage = 10f; // 변수를 선언할거에요 -> 공격력(10.0)을 attackDamage에
[SerializeField] protected int expReward = 10; [SerializeField] protected int expReward = 10; // 변수를 선언할거에요 -> 처치 시 경험치 보상(10)을 expReward에
[SerializeField] protected float moveSpeed = 3.5f; [SerializeField] protected float moveSpeed = 3.5f; // 변수를 선언할거에요 -> 이동 속도(3.5)를 moveSpeed에
protected float currentHP; protected float currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 currentHP를
public event Action<float, float> OnHealthChanged; public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> 체력이 변할 때 알릴 OnHealthChanged를
[Header("전투 / 무기 (선택사항)")] [Header("전투 / 무기 (선택사항)")] // 인스펙터 창에 제목을 표시할거에요 -> 전투 / 무기 (선택사항) 을
// ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨) // ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨)
[SerializeField] protected MonsterWeapon myWeapon; [SerializeField] protected MonsterWeapon myWeapon; // 변수를 선언할거에요 -> 몬스터가 장착한 무기 스크립트를 myWeapon에
[Header("피격 / 사망 / 대기 애니메이션")] [Header("피격 / 사망 / 대기 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 피격 / 사망 / 대기 애니메이션 을
[SerializeField] protected string Monster_Idle = "Monster_Idle"; [SerializeField] protected string Monster_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름("Monster_Idle")을 Monster_Idle에
[SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; [SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; // 변수를 선언할거에요 -> 피격 애니메이션 이름("Monster_GetDamage")을 Monster_GetDamage에
[SerializeField] protected string Monster_Die = "Monster_Die"; [SerializeField] protected string Monster_Die = "Monster_Die"; // 변수를 선언할거에요 -> 사망 애니메이션 이름("Monster_Die")을 Monster_Die에
protected Animator animator; protected Animator animator; // 변수를 선언할거에요 -> 애니메이션 제어 컴포넌트를 담을 animator를
protected NavMeshAgent agent; protected NavMeshAgent agent; // 변수를 선언할거에요 -> 길찾기 에이전트 컴포넌트를 담을 agent를
protected AudioSource audioSource; protected AudioSource audioSource; // 변수를 선언할거에요 -> 소리 재생 컴포넌트를 담을 audioSource를
protected bool isHit, isDead, isAttacking;
public bool IsAggroed { get; protected set; } // ⭐ [핵심] 자식 클래스에서 공유할 상태 변수들 (중복 선언 방지)
protected bool isHit;
protected bool isDead;
protected bool isAttacking;
[Header("AI 설정")] public bool IsAggroed { get; protected set; } // 프로퍼티를 선언할거에요 -> 어그로(전투) 상태 여부를 외부에서 읽기만 가능하게 IsAggroed에
[SerializeField] protected float attackRestDuration = 1.5f;
protected bool isResting;
public static System.Action<int> OnMonsterKilled; [Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을
[SerializeField] protected float attackRestDuration = 1.5f; // 변수를 선언할거에요 -> 공격 후 대기 시간(1.5초)을 attackRestDuration에
protected bool isResting; // 변수를 선언할거에요 -> 휴식 중인지 여부를 저장할 isResting을
[Header("공통 사운드/이펙트")] public static System.Action<int> OnMonsterKilled; // 정적 이벤트를 선언할거에요 -> 몬스터 처치 시 경험치를 전달할 OnMonsterKilled를
[SerializeField] protected AudioClip hitSound, deathSound;
[SerializeField] protected GameObject deathEffectPrefab; [Header("공통 사운드/이펙트")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 사운드/이펙트 를
[SerializeField] protected ParticleSystem hitEffect; [SerializeField] protected AudioClip hitSound, deathSound; // 변수를 선언할거에요 -> 피격음과 사망음 오디오 클립을
[SerializeField] protected Transform impactSpawnPoint; [SerializeField] protected GameObject deathEffectPrefab; // 변수를 선언할거에요 -> 사망 시 재생할 이펙트 프리팹을
[SerializeField] protected ParticleSystem hitEffect; // 변수를 선언할거에요 -> 피격 시 재생할 파티클 시스템을
[SerializeField] protected Transform impactSpawnPoint; // 변수를 선언할거에요 -> 이펙트가 생성될 위치를
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 생명주기 // 생명주기
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
protected virtual void Awake() protected virtual void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 호출되는 Awake를
{ {
mobRenderer = GetComponentInChildren<Renderer>(); mobRenderer = GetComponentInChildren<Renderer>(); // 컴포넌트를 가져올거에요 -> 자식 오브젝트의 렌더러를 mobRenderer에
animator = GetComponent<Animator>(); animator = GetComponent<Animator>(); // 컴포넌트를 가져올거에요 -> 내 몸의 애니메이터를 animator에
agent = GetComponent<NavMeshAgent>(); agent = GetComponent<NavMeshAgent>(); // 컴포넌트를 가져올거에요 -> 내 몸의 길찾기 에이전트를 agent에
audioSource = GetComponent<AudioSource>(); audioSource = GetComponent<AudioSource>(); // 컴포넌트를 가져올거에요 -> 내 몸의 오디오 소스를 audioSource에
if (agent != null) agent.speed = moveSpeed; if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 설정할거에요 -> 에이전트가 있다면 이동 속도를 moveSpeed로
} }
protected virtual void OnEnable() protected virtual void OnEnable() // 함수를 실행할거에요 -> 오브젝트가 활성화될 때 호출되는 OnEnable을
{ {
playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // 오브젝트를 찾을거에요 -> "Player" 태그를 가진 오브젝트의 위치를 playerTransform에
if (mobRenderer != null) mobRenderer.enabled = true; if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 보이게 켜기(true)를
Init(); Init(); // 함수를 실행할거에요 -> 자식 클래스에서 정의할 초기화 함수 Init을
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); 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; OnManagedUpdate(); // 기본적으로 매니저 업데이트 로직을 수행 (일반 몬스터용)
IsAggroed = false; }
currentHP = maxHP;
OnHealthChanged?.Invoke(currentHP, maxHP);
Collider col = GetComponent<Collider>(); public void ResetStats() // 함수를 선언할거에요 -> 몬스터 상태를 초기화하는 ResetStats를
if (col != null) col.enabled = true; {
isDead = false; // 상태를 바꿀거에요 -> 사망 상태를 거짓(false)으로
IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로
currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로
OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 변경되었음을 UI 등에
if (agent != null) agent.speed = moveSpeed; // 스피드 초기화 Collider col = GetComponent<Collider>(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에
if (col != null) col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 다시 켜기(true)를
OnResetStats(); if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 속도를 초기화하기를
OnResetStats(); // 함수를 실행할거에요 -> 자식 클래스의 추가 초기화 함수 OnResetStats를
} }
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 추상 메서드 (자식이 반드시 구현) // 추상 메서드 (자식이 반드시 구현)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
protected virtual void Init() { } protected virtual void Init() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 초기화 함수 Init을
protected abstract void ExecuteAILogic(); protected abstract void ExecuteAILogic(); // 추상 함수를 선언할거에요 -> 자식에서 반드시 구현해야 할 AI 로직 ExecuteAILogic을
protected virtual void OnResetStats() { } 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; agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고
if (IsAggroed && playerTransform != null) 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(); StopMovement(); // 함수를 실행할거에요 -> 움직임을 멈추는 StopMovement를
if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; // 조건이 맞으면 실행할거에요 -> 렌더러가 켜져 있다면 끄기(false)를 (최적화)
return; return; // 중단할거에요 -> AI 로직을 실행하지 않도록 함수를
} }
if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 꺼져 있다면 다시 켜기(true)를
if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } // 조건이 맞으면 실행할거에요 -> 렌더러가 화면에 보이지 않는다면 멈추고 중단하기를
if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; 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.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
agent.velocity = Vector3.zero; agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 0으로
} }
if (animator != null) if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면
{ {
animator.SetFloat("Speed", 0f); animator.SetFloat("Speed", 0f); // 값을 전달할거에요 -> 속도 파라미터를 0으로
animator.Play(Monster_Idle); 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; if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
IsAggroed = true; IsAggroed = true; // 상태를 바꿀거에요 -> 공격받았으니 어그로 상태를 참(true)으로
currentHP -= damage; currentHP -= damage; // 값을 뺄거에요 -> 체력에서 데미지(damage)만큼을
OnHealthChanged?.Invoke(currentHP, maxHP); OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 깎였음을
if (currentHP <= 0) { Die(); return; } if (currentHP <= 0) { Die(); return; } // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 함수(Die)를
if (!isHit) StartHit(); if (!isHit) StartHit(); // 조건이 맞으면 실행할거에요 -> 피격 상태가 아니라면 피격 연출(StartHit)을
} }
protected virtual void StartHit() protected virtual void StartHit() // 함수를 선언할거에요 -> 피격 연출을 시작하는 StartHit을
{ {
isHit = true; isHit = true; // 상태를 바꿀거에요 -> 피격 중 상태를 참(true)으로
isAttacking = false; isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
isResting = false; isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
StopAllCoroutines(); StopAllCoroutines(); // 중단할거에요 -> 실행 중인 모든 코루틴을
OnStartHit(); OnStartHit(); // 함수를 실행할거에요 -> 자식 클래스의 추가 피격 처리 OnStartHit을
if (agent && agent.isOnNavMesh) if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면
{ {
agent.isStopped = true; agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
agent.velocity = Vector3.zero; agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
} }
animator.Play(Monster_GetDamage, 0, 0f); animator.Play(Monster_GetDamage, 0, 0f); // 재생할거에요 -> 피격 애니메이션(Monster_GetDamage)을
if (hitEffect) hitEffect.Play(); if (hitEffect) hitEffect.Play(); // 조건이 맞으면 실행할거에요 -> 피격 이펙트가 있다면 재생하기를
if (hitSound) audioSource.PlayOneShot(hitSound); 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; isHit = false; // 상태를 바꿀거에요 -> 피격 중 상태를 거짓(false)으로
if (agent && agent.isOnNavMesh) agent.isStopped = false; if (agent && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 다시 움직이게(false) 하기를
} }
protected virtual void Die() protected virtual void Die() // 함수를 선언할거에요 -> 몬스터 사망을 처리하는 Die를
{ {
if (isDead) return; if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽은 상태라면
isDead = true; isDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참(true)으로
IsAggroed = false; IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로
OnDie(); OnDie(); // 함수를 실행할거에요 -> 자식 클래스의 추가 사망 처리 OnDie를
OnMonsterKilled?.Invoke(expReward); OnMonsterKilled?.Invoke(expReward); // 이벤트를 알릴거에요 -> 몬스터 처치와 경험치 보상(expReward)을
Collider col = GetComponent<Collider>(); Collider col = GetComponent<Collider>(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에
if (col != null) col.enabled = false; if (col != null) col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 끄기(false)를 (시체 밟기 방지)
if (agent && agent.isOnNavMesh) if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면
{ {
agent.isStopped = true; agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
agent.velocity = Vector3.zero; agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
} }
animator.Play(Monster_Die, 0, 0f); animator.Play(Monster_Die, 0, 0f); // 재생할거에요 -> 사망 애니메이션(Monster_Die)을
if (deathSound) audioSource.PlayOneShot(deathSound); if (deathSound) audioSource.PlayOneShot(deathSound); // 조건이 맞으면 실행할거에요 -> 사망 사운드가 있다면 재생하기를
Invoke("ReturnToPool", 1.5f); Invoke("ReturnToPool", 1.5f); // 예약을 걸거에요 -> 1.5초 뒤에 풀로 반환하는 ReturnToPool 함수를
} }
protected virtual void OnDie() { } protected virtual void OnDie() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 사망 처리 함수 OnDie를
public bool IsDead => isDead; public bool IsDead => isDead; // 프로퍼티를 선언할거에요 -> 외부에서 사망 여부를 확인할 수 있는 IsDead를
protected void ReturnToPool() => gameObject.SetActive(false); protected void ReturnToPool() => gameObject.SetActive(false); // 함수를 선언할거에요 -> 오브젝트를 비활성화해서 풀로 돌려보내는 ReturnToPool을
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ⭐ 공격 이벤트 // ⭐ 공격 이벤트
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public virtual void OnAttackStart() // ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언
public virtual void OnAttackStart() // 함수를 선언할거에요 -> 공격 시작 시 호출되는 OnAttackStart를
{ {
isAttacking = true; isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
isResting = false; 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; isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴(RestAfterAttack)을
} }
protected virtual IEnumerator RestAfterAttack() protected virtual IEnumerator RestAfterAttack() // 코루틴 함수를 선언할거에요 -> 공격 후 잠시 쉬는 RestAfterAttack을
{ {
isResting = true; isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로
yield return new WaitForSeconds(attackRestDuration); yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간(attackRestDuration)만큼
isResting = false; isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
} }
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ⭐ 상태 이상 시스템 (추가된 부분) // ⭐ 상태 이상 시스템
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/// <summary> public void ApplyStatusEffect(StatusEffectType type, float damage, float duration) // 함수를 선언할거에요 -> 상태이상을 적용하는 ApplyStatusEffect를
/// 상태이상 적용 (PlayerArrow에서 호출)
/// </summary>
public void ApplyStatusEffect(StatusEffectType type, float damage, float duration)
{ {
if (isDead) return; if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
switch (type) switch (type) // 분기할거에요 -> 상태이상 종류(type)에 따라
{ {
case StatusEffectType.Burn: case StatusEffectType.Burn: // 조건이 맞으면 실행할거에요 -> 화상(Burn)이라면
StartCoroutine(BurnCoroutine(damage, duration)); StartCoroutine(BurnCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 화상 효과를 주는 BurnCoroutine을
break; break;
case StatusEffectType.Slow: case StatusEffectType.Slow: // 조건이 맞으면 실행할거에요 -> 슬로우(Slow)라면
StartCoroutine(SlowCoroutine(damage, duration)); StartCoroutine(SlowCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 느리게 만드는 SlowCoroutine을
break; break;
case StatusEffectType.Poison: case StatusEffectType.Poison: // 조건이 맞으면 실행할거에요 -> 독(Poison)이라면
StartCoroutine(PoisonCoroutine(damage, duration)); StartCoroutine(PoisonCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 독 효과를 주는 PoisonCoroutine을
break; break;
case StatusEffectType.Shock: case StatusEffectType.Shock: // 조건이 맞으면 실행할거에요 -> 충격(Shock)이라면
TakeDamage(damage); // 충격 데미지 즉시 적용 TakeDamage(damage); // 함수를 실행할거에요 -> 충격 데미지 즉시 적용하기를
StartCoroutine(StunCoroutine(0.5f)); // 0.5초 스턴 StartCoroutine(StunCoroutine(0.5f)); // 코루틴을 실행할거에요 -> 0.5초간 기절시키는 StunCoroutine을
break; break;
} }
} }
/// <summary> private IEnumerator BurnCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 화상 효과를 처리할 BurnCoroutine을
/// 화상: 일정 시간 동안 0.5초마다 틱 데미지
/// </summary>
private IEnumerator BurnCoroutine(float tickDamage, float duration)
{ {
float elapsed = 0f; float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
float tickInterval = 0.5f; float tickInterval = 0.5f; // 변수를 초기화할거에요 -> 데미지 간격을 0.5초로
while (elapsed < duration) while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안
{ {
TakeDamage(tickDamage * tickInterval); TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를
yield return new WaitForSeconds(tickInterval); yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 0.5초의 시간을
elapsed += tickInterval; elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 0.5초를
} }
} }
/// <summary> private IEnumerator SlowCoroutine(float amount, float duration) // 코루틴 함수를 선언할거에요 -> 슬로우 효과를 처리할 SlowCoroutine을
/// 슬로우: 이동속도를 일시적으로 감소
/// </summary>
private IEnumerator SlowCoroutine(float amount, float duration)
{ {
// NavMeshAgent가 있는 경우 속도 조절 if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
if (agent != null)
{ {
float originalSpeed = agent.speed; float originalSpeed = agent.speed; // 값을 저장할거에요 -> 원래 속도를 originalSpeed에
agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); // 값을 변경할거에요 -> 현재 속도를 비율(amount)만큼 줄여서
yield return new WaitForSeconds(duration); yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼
agent.speed = originalSpeed; // 원래 속도로 복구 agent.speed = originalSpeed; // 값을 복구할거에요 -> 속도를 원래대로
} }
else else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면
{ {
// NavMeshAgent가 없으면 그냥 대기 yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를
yield return new WaitForSeconds(duration);
} }
} }
/// <summary> private IEnumerator PoisonCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 독 효과를 처리할 PoisonCoroutine을
/// 독: 지속 데미지 (1초마다)
/// </summary>
private IEnumerator PoisonCoroutine(float tickDamage, float duration)
{ {
float elapsed = 0f; float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
float tickInterval = 1f; float tickInterval = 1f; // 변수를 초기화할거에요 -> 데미지 간격을 1초로
while (elapsed < duration) while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안
{ {
TakeDamage(tickDamage * tickInterval); TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를
yield return new WaitForSeconds(tickInterval); yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 1초의 시간을
elapsed += tickInterval; elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 1초를
} }
} }
/// <summary> private IEnumerator StunCoroutine(float duration) // 코루틴 함수를 선언할거에요 -> 기절 효과를 처리할 StunCoroutine을
/// 스턴: 짧은 시간 동안 행동 정지
/// </summary>
private IEnumerator StunCoroutine(float duration)
{ {
if (agent != null) if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
{ {
agent.isStopped = true; agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
if (animator != null) animator.speed = 0; // 애니메이션도 멈춤 (선택사항) 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; agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고
if (animator != null) animator.speed = 1; if (animator != null) animator.speed = 1; // 조건이 맞으면 실행할거에요 -> 애니메이션 속도를 정상(1)으로
} }
} }
else else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면
{ {
yield return new WaitForSeconds(duration); yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를
} }
} }
} }

View File

@ -1,185 +1,182 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.AI; using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
using System.Collections.Generic; // ✅ 리스트 사용을 위해 필수 추가 using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
public class MeleeMonster : MonsterClass public class MeleeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 MeleeMonster를
{ {
[Header("=== 근접 공격 설정 ===")] [Header("=== 근접 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 근접 공격 설정 === 을
//[SerializeField] private MonsterWeapon myWeapon; // ✅ 주석 해제됨 (이제 무기 연결 가능!) [SerializeField] private float attackRange = 2f; // 변수를 선언할거에요 -> 공격 사거리(2.0)를 attackRange에
[SerializeField] private float attackRange = 2f; [SerializeField] private float attackDelay = 1.5f; // 변수를 선언할거에요 -> 공격 딜레이(1.5초)를 attackDelay에
[SerializeField] private float attackDelay = 1.5f;
// 🎁 [수정] 단일 아이템 -> 리스트로 변경 [Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을
[Header("=== 드랍 아이템 설정 ===")] [Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")] [SerializeField] private List<GameObject> dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍할 아이템 프리팹 목록을 dropItemPrefabs에
[SerializeField] private List<GameObject> dropItemPrefabs;
[Tooltip("아이템이 나올 확률 (0 ~ 100%)")] [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[Range(0, 100)][SerializeField] private float dropChance = 30f; // 기본 30% [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에
private float lastAttackTime; private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에
[Header("공격 / 이동 애니메이션")] [Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을
[SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; [SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; // 배열을 선언할거에요 -> 공격 애니메이션 이름들을 attackAnimations에
[SerializeField] private string Monster_Walk = "Monster_Walk"; [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에
[Header("AI 상세 설정")] [Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을
[SerializeField] private float stopBuffer = 0.3f; [SerializeField] private float stopBuffer = 0.3f; // 변수를 선언할거에요 -> 정지 여유 거리(0.3)를 stopBuffer에
[SerializeField] private float patrolRadius = 5f; [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
[SerializeField] private float patrolInterval = 2f; [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 대기 시간(2.0)을 patrolInterval에
private float nextPatrolTime; private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
private float repathInterval = 0.3f; private float repathInterval = 0.3f; // 변수를 선언할거에요 -> 경로 갱신 주기(0.3초)를 repathInterval에
private float nextRepathTime; private float nextRepathTime; // 변수를 선언할거에요 -> 다음 경로 갱신 시간을 nextRepathTime에
private int attackIndex; private int attackIndex; // 변수를 선언할거에요 -> 현재 공격 애니메이션 인덱스를 attackIndex에
private bool isPlayerInZone; private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어가 감지 구역에 있는지 여부를 isPlayerInZone에
protected override void Init() protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
{ {
if (agent != null) agent.stoppingDistance = attackRange - 0.4f; if (agent != null) agent.stoppingDistance = attackRange - 0.4f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 사거리보다 약간 짧게
if (animator != null) animator.applyRootMotion = false; 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) if (isPlayerInZone || distance <= attackRange * 2f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면
HandlePlayerTarget(); HandlePlayerTarget(); // 함수를 실행할거에요 -> 추격 및 공격 처리를 하는 HandlePlayerTarget을
else else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
Patrol(); 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) if (distance <= attackRange - stopBuffer) // 조건이 맞으면 실행할거에요 -> 사거리 안쪽으로 충분히 들어왔다면
TryAttack(); TryAttack(); // 함수를 실행할거에요 -> 공격을 시도하는 TryAttack을
else if (Time.time >= nextRepathTime) else if (Time.time >= nextRepathTime) // 조건이 맞으면 실행할거에요 -> 경로 갱신 시간이 되었다면
{ {
if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position); if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
nextRepathTime = Time.time + repathInterval; nextRepathTime = Time.time + repathInterval; // 값을 갱신할거에요 -> 다음 경로 갱신 시간을
} }
} }
void TryAttack() void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을
{ {
if (Time.time < lastAttackTime + attackDelay) return; if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 아직 쿨타임이 안 지났다면
lastAttackTime = Time.time; lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로
string attackName = attackAnimations[attackIndex]; string attackName = attackAnimations[attackIndex]; // 값을 가져올거에요 -> 현재 인덱스의 공격 애니메이션 이름을
attackIndex = (attackIndex + 1) % attackAnimations.Length; attackIndex = (attackIndex + 1) % attackAnimations.Length; // 값을 갱신할거에요 -> 다음 공격 인덱스로 (순환)
isAttacking = true; isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
if (agent.isOnNavMesh) if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 바닥에 있다면
{ {
agent.isStopped = true; agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
agent.velocity = Vector3.zero; 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) if (agent.velocity.magnitude < 0.1f) // 조건이 맞으면 실행할거에요 -> 거의 멈춰 있다면
animator.Play(Monster_Idle); animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을
else else // 조건이 틀리면 실행할거에요 -> 움직이고 있다면
animator.Play(Monster_Walk); 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), Random.Range(-patrolRadius, patrolRadius),
0, 0,
Random.Range(-patrolRadius, patrolRadius) Random.Range(-patrolRadius, patrolRadius)
); );
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 랜덤 지점이 NavMesh 위라면
if (agent.isOnNavMesh) agent.SetDestination(hit.position); 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; isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
isResting = false; isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
if (myWeapon != null) myWeapon.EnableHitBox(); if (myWeapon != null) myWeapon.EnableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 켜기를
} }
public override void OnAttackEnd() public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를
{ {
if (myWeapon != null) myWeapon.DisableHitBox(); if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 끄기를
isAttacking = false; isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴을
} }
protected override IEnumerator RestAfterAttack() protected override IEnumerator RestAfterAttack() // 코루틴 함수를 덮어씌워 실행할거에요 -> 공격 후 휴식하는 RestAfterAttack을
{ {
isResting = true; isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로
yield return new WaitForSeconds(attackRestDuration); yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간만큼
isResting = false; 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. 리스트에 아이템이 하나라도 있는지 확인 // 1. 리스트에 아이템이 하나라도 있는지 확인
if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 드랍 테이블이 비어있지 않다면
{ {
// 2. 확률 체크 (0 ~ 100) // 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번 ~ 마지막 번호 중 하나) // 3. 리스트에서 랜덤하게 하나 뽑기 (0번 ~ 마지막 번호 중 하나)
int randomIndex = Random.Range(0, dropItemPrefabs.Count); int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 아이템 리스트 인덱스를 랜덤으로
GameObject selectedItem = dropItemPrefabs[randomIndex]; GameObject selectedItem = dropItemPrefabs[randomIndex]; // 오브젝트를 가져올거에요 -> 선택된 아이템 프리팹을
if (selectedItem != null) if (selectedItem != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면
{ {
Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity); Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에
// Debug.Log($"🎉 아이템 드랍! ({selectedItem.name})");
} }
} }
} }
} }
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)으로
} }
} }

View File

@ -1,213 +1,208 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.AI; using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
/// <summary> /// <summary>
/// 통합 원거리 몬스터 (키 작은 플레이어 머리 조준 기능 추가) /// 통합 원거리 몬스터 (키 작은 플레이어 머리 조준 기능 추가)
/// 파일 이름을 반드시 [ UniversalRangedMonster.cs ] 로 맞춰주세요! /// 파일 이름을 반드시 [ UniversalRangedMonster.cs ] 로 맞춰주세요!
/// </summary> /// </summary>
public class UniversalRangedMonster : MonsterClass public class UniversalRangedMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 UniversalRangedMonster를
{ {
public enum AttackStyle { Straight, Lob } public enum AttackStyle { Straight, Lob } // 열거형을 정의할거에요 -> 공격 방식(직선, 곡사)을 정의하는 AttackStyle을
[Header("=== 🏹 공격 스타일 선택 ===")] [Header("=== 🏹 공격 스타일 선택 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 🏹 공격 스타일 선택 === 을
[SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; [SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; // 변수를 선언할거에요 -> 공격 스타일(기본 직선)을 attackStyle에
[Header("공통 설정")] [Header("공통 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 설정 을
[SerializeField] private GameObject projectilePrefab; [SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 발사체 프리팹을 projectilePrefab에
[SerializeField] private Transform firePoint; [SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 발사 위치를 firePoint에
[SerializeField] private float attackRange = 10f; [SerializeField] private float attackRange = 10f; // 변수를 선언할거에요 -> 공격 사거리(10.0)를 attackRange에
[SerializeField] private float attackDelay = 2f; [SerializeField] private float attackDelay = 2f; // 변수를 선언할거에요 -> 공격 딜레이(2.0초)를 attackDelay에
[SerializeField] private float detectRange = 15f; [SerializeField] private float detectRange = 15f; // 변수를 선언할거에요 -> 인식 거리(15.0)를 detectRange에
[Header("🔹 직선 발사 설정 (활/총)")] [Header("🔹 직선 발사 설정 (활/총)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔹 직선 발사 설정 (활/총) 을
[SerializeField] private float projectileSpeed = 20f; [SerializeField] private float projectileSpeed = 20f; // 변수를 선언할거에요 -> 투사체 속도(20.0)를 projectileSpeed에
[SerializeField] private float minDistance = 5f; [SerializeField] private float minDistance = 5f; // 변수를 선언할거에요 -> 최소 거리(도망가는 거리)를 minDistance에
[Header("🔸 곡사 투척 설정 (돌/폭탄)")] [Header("🔸 곡사 투척 설정 (돌/폭탄)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔸 곡사 투척 설정 (돌/폭탄) 을
[Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")] [Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private bool usePreciseLob = true; [SerializeField] private bool usePreciseLob = true; // 변수를 선언할거에요 -> 정밀 곡사 사용 여부를 usePreciseLob에
[Tooltip("던지는 각도 (45도가 최대 사거리)")] [Tooltip("던지는 각도 (45도가 최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[Range(15f, 75f)][SerializeField] private float launchAngle = 45f; [Range(15f, 75f)][SerializeField] private float launchAngle = 45f; // 변수를 선언할거에요 -> 발사 각도(45도)를 launchAngle에
// ⭐ [추가됨] 조준 높이 보정 (키 작은 플레이어 해결용) // ⭐ [추가됨] 조준 높이 보정 (키 작은 플레이어 해결용)
[Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")] [Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private float aimHeight = 1.2f; [SerializeField] private float aimHeight = 1.2f; // 변수를 선언할거에요 -> 조준 높이 오프셋(1.2)을 aimHeight에
[Header("🏃‍♂️ 도망 설정")] [Header("🏃‍♂️ 도망 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 🏃‍♂️ 도망 설정 을
[SerializeField] private float fleeDistance = 5f; [SerializeField] private float fleeDistance = 5f; // 변수를 선언할거에요 -> 도망가는 거리(5.0)를 fleeDistance에
[Header("애니메이션 & 기타")] [Header("애니메이션 & 기타")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 & 기타 를
[SerializeField] private float throwForce = 15f; // (정밀 모드 꺼졌을 때 사용) [SerializeField] private float throwForce = 15f; // 변수를 선언할거에요 -> 기본 투척 힘을 throwForce에
[SerializeField] private float throwUpward = 5f; // (정밀 모드 꺼졌을 때 사용) [SerializeField] private float throwUpward = 5f; // 변수를 선언할거에요 -> 기본 상향 힘을 throwUpward에
[SerializeField] private float reloadTime = 2.0f; [SerializeField] private float reloadTime = 2.0f; // 변수를 선언할거에요 -> 장전 시간(2.0초)을 reloadTime에
[SerializeField] private GameObject handModel; [SerializeField] private GameObject handModel; // 변수를 선언할거에요 -> 손에 든 무기 모델을 handModel에
[SerializeField] private bool aimAtPlayer = true; [SerializeField] private bool aimAtPlayer = true; // 변수를 선언할거에요 -> 플레이어 조준 여부를 aimAtPlayer에
[SerializeField] private string attackAnim = "Monster_Attack"; [SerializeField] private string attackAnim = "Monster_Attack"; // 변수를 선언할거에요 -> 공격 애니메이션 이름을 attackAnim에
[SerializeField] private string walkAnim = "Monster_Walk"; [SerializeField] private string walkAnim = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 walkAnim에
[SerializeField] private float patrolRadius = 5f; [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
[SerializeField] private float patrolInterval = 3f; [SerializeField] private float patrolInterval = 3f; // 변수를 선언할거에요 -> 순찰 간격(3.0초)을 patrolInterval에
private float lastAttackTime; private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에
private float nextPatrolTime; private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
// private bool isPlayerInZone; private bool isReloading = false; // 변수를 선언할거에요 -> 장전 중 여부를 isReloading에
private bool isReloading = false;
protected override void Init() protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
{ {
if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; } if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; } // 조건이 맞으면 설정할거에요 -> 정지 거리를 사거리의 80%로, 속도를 3.5로
if (animator != null) animator.applyRootMotion = false; if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로
} }
protected override void ExecuteAILogic() protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
{ {
if (isHit || isAttacking || isResting || isReloading) return; if (isHit || isAttacking || isResting || isReloading) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식, 장전 중이라면
if (isAttacking && animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) OnAttackEnd(); 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); if (dist <= detectRange) HandleCombat(dist); // 조건이 맞으면 실행할거에요 -> 감지 거리 이내라면 전투 처리(HandleCombat)를
else else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
{ {
if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; } if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; } // 조건이 맞으면 실행할거에요 -> 추격 중이었다면 경로를 취소하고 순찰을 준비하기를
Patrol(); Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
UpdateMovementAnimation(); UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 갱신을
} }
} }
void HandleCombat(float dist) void HandleCombat(float dist) // 함수를 선언할거에요 -> 거리별 전투 행동을 정하는 HandleCombat을
{ {
if (attackStyle == AttackStyle.Straight && dist < minDistance) RetreatFromPlayer(); if (attackStyle == AttackStyle.Straight && dist < minDistance) RetreatFromPlayer(); // 조건이 맞으면 실행할거에요 -> 직선 공격 타입이고 너무 가까우면 도망가기를
else if (dist <= attackRange) TryAttack(); else if (dist <= attackRange) TryAttack(); // 조건이 맞으면 실행할거에요 -> 사거리 안이라면 공격 시도를
else { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); UpdateMovementAnimation(); } } else { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); UpdateMovementAnimation(); } } // 그 외엔 실행할거에요 -> 플레이어를 향해 추격하기를
} }
void TryAttack() void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을
{ {
if (Time.time < lastAttackTime + attackDelay) return; if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 쿨타임이 안 지났다면
lastAttackTime = Time.time; lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로
isAttacking = true; 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; Vector3 lookDir = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 바라보는 방향을
lookDir.y = 0; lookDir.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게
if (lookDir != Vector3.zero) transform.rotation = Quaternion.LookRotation(lookDir); 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<Projectile>(out var proj)) if (obj.TryGetComponent<Projectile>(out var proj)) // 조건이 맞으면 실행할거에요 -> 투사체 스크립트가 있다면
{ {
float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f; float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f; // 값을 결정할거에요 -> 직선이면 속도를, 곡사면 0(물리)을
proj.Initialize(transform.forward, speed, attackDamage); proj.Initialize(transform.forward, speed, attackDamage); // 초기화할거에요 -> 방향, 속도, 데미지 정보를 전달해서
} }
if (attackStyle == AttackStyle.Lob) if (attackStyle == AttackStyle.Lob) // 조건이 맞으면 실행할거에요 -> 곡사 공격 타입이라면
{ {
Rigidbody rb = obj.GetComponent<Rigidbody>(); Rigidbody rb = obj.GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 투사체의 리지드바디를
if (rb) 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)를 기준으로 탄도 계산 // 보정된 위치(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; Vector3 dir = aimAtPlayer ? (targetPos - firePoint.position).normalized : transform.forward; // 방향을 결정할거에요 -> 타겟 방향 혹은 정면으로
// dir.y = 0; // 높이 조준을 위해 y제거 주석 처리 (원하면 해제) rb.AddForce(dir * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 방향과 힘을 적용해서 던지기를
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; Vector3 dir = target - origin; // 벡터를 계산할거에요 -> 목표까지의 거리 벡터를
float height = dir.y; float height = dir.y; // 값을 저장할거에요 -> 높이 차이를
dir.y = 0; dir.y = 0; // 값을 바꿀거에요 -> 수평 거리 계산을 위해 y를 0으로
float dist = dir.magnitude; float dist = dir.magnitude; // 값을 저장할거에요 -> 수평 거리를
float a = angle * Mathf.Deg2Rad; float a = angle * Mathf.Deg2Rad; // 값을 변환할거에요 -> 각도를 라디안으로
dir.y = dist * Mathf.Tan(a); dir.y = dist * Mathf.Tan(a); // 값을 계산할거에요 -> 탄젠트를 이용해 높이를
dist += height / Mathf.Tan(a); dist += height / Mathf.Tan(a); // 값을 보정할거에요 -> 높이 차이에 따른 거리 보정을
float gravity = Physics.gravity.magnitude; float gravity = Physics.gravity.magnitude; // 값을 가져올거에요 -> 중력 가속도를
float velocitySq = (gravity * dist * dist) / (2 * Mathf.Pow(Mathf.Cos(a), 2) * (dist * Mathf.Tan(a) - height)); // 물리 공식: 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); float velocity = Mathf.Sqrt(velocitySq); // 값을 계산할거에요 -> 제곱근을 구해서 실제 속도를
return dir.normalized * velocity; return dir.normalized * velocity; // 반환할거에요 -> 방향 벡터에 속도를 곱해서
} }
// ... (나머지 함수들은 기존과 동일) ... void RetreatFromPlayer() // 함수를 선언할거에요 -> 플레이어로부터 도망가는 RetreatFromPlayer를
void RetreatFromPlayer()
{ {
if (agent.pathPending || (agent.remainingDistance > 0.5f && agent.velocity.magnitude > 0.1f)) { UpdateMovementAnimation(); return; } if (agent.pathPending || (agent.remainingDistance > 0.5f && agent.velocity.magnitude > 0.1f)) { UpdateMovementAnimation(); return; } // 조건이 맞으면 중단할거에요 -> 이미 이동 중이라면
Vector3 dir = (transform.position - playerTransform.position).normalized; Vector3 dir = (transform.position - playerTransform.position).normalized; // 벡터를 계산할거에요 -> 플레이어 반대 방향을
Vector3 pos = transform.position + dir * fleeDistance; 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); } } if (NavMesh.SamplePosition(pos, out NavMeshHit hit, 5f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 그곳으로 이동하기를
UpdateMovementAnimation(); UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션 갱신을
} }
IEnumerator ReloadRoutine() IEnumerator ReloadRoutine() // 코루틴 함수를 선언할거에요 -> 무기 장전(재생성)을 처리하는 ReloadRoutine을
{ {
isReloading = true; isReloading = true; // 상태를 바꿀거에요 -> 장전 중 상태를 참으로
yield return new WaitForSeconds(reloadTime); yield return new WaitForSeconds(reloadTime); // 기다릴거에요 -> 장전 시간만큼
if (handModel != null) handModel.SetActive(true); if (handModel != null) handModel.SetActive(true); // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 다시 보이게
isReloading = false; isReloading = false; // 상태를 바꿀거에요 -> 장전 중 상태를 거짓으로
} }
public override void OnAttackEnd() public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를
{ {
isAttacking = false; isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓으로
if (animator != null) animator.Play(Monster_Idle); if (animator != null) animator.Play(Monster_Idle); // 조건이 맞으면 실행할거에요 -> 대기 애니메이션으로 복귀를
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); 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; } 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)); 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); } } if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 이동하기를
nextPatrolTime = Time.time + patrolInterval; nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
} }
void UpdateMovementAnimation() void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션 갱신 함수를
{ {
if (isAttacking || isHit || isResting) return; if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
if (agent.velocity.magnitude > 0.1f) animator.Play(walkAnim); else animator.Play(Monster_Idle); 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; }
} }

View File

@ -1,25 +1,25 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
/// <summary> /// <summary>
/// 만능 투사체 (화살, 마법, 돌맹이 공용) /// 만능 투사체 (화살, 마법, 돌맹이 공용)
/// </summary> /// </summary>
public class Projectile : MonoBehaviour public class Projectile : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 Projectile을
{ {
[Header("기본 설정")] [Header("기본 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 기본 설정 을
[SerializeField] private float lifetime = 5f; // 수명 (5초 뒤 삭제) [SerializeField] private float lifetime = 5f; // 변수를 선언할거에요 -> 수명(5.0초)을 lifetime에
[SerializeField] private GameObject hitEffectPrefab; // 맞으면 나오는 이펙트 (폭발 등) [SerializeField] private GameObject hitEffectPrefab; // 변수를 선언할거에요 -> 충돌 이펙트 프리팹을 hitEffectPrefab에
[SerializeField] private bool destroyOnHit = true; // 맞으면 사라짐? (관통 아니면 true) [SerializeField] private bool destroyOnHit = true; // 변수를 선언할거에요 -> 충돌 시 삭제 여부를 destroyOnHit에
// 내부 변수 // 내부 변수
private Vector3 _direction; private Vector3 _direction; // 변수를 선언할거에요 -> 날아갈 방향을 _direction에
private float _speed; private float _speed; // 변수를 선언할거에요 -> 이동 속도를 _speed에
private float _damage; private float _damage; // 변수를 선언할거에요 -> 공격 데미지를 _damage에
private bool _isInitialized = false; private bool _isInitialized = false; // 변수를 초기화할거에요 -> 초기화 여부를 거짓(false)으로
private Rigidbody _rigidbody; private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트 _rigidbody를
private void Awake() private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를
{ {
_rigidbody = GetComponent<Rigidbody>(); _rigidbody = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 리지드바디를
} }
/// <summary> /// <summary>
@ -28,79 +28,79 @@ public class Projectile : MonoBehaviour
/// <param name="direction">날아갈 방향</param> /// <param name="direction">날아갈 방향</param>
/// <param name="speed">속도 (0이면 물리력으로 날아감)</param> /// <param name="speed">속도 (0이면 물리력으로 날아감)</param>
/// <param name="damage">줄 데미지</param> /// <param name="damage">줄 데미지</param>
public void Initialize(Vector3 direction, float speed, float damage) public void Initialize(Vector3 direction, float speed, float damage) // 함수를 선언할거에요 -> 투사체 정보를 설정하는 Initialize를
{ {
_direction = direction.normalized; _direction = direction.normalized; // 값을 저장할거에요 -> 방향 벡터를 정규화해서
_speed = speed; _speed = speed; // 값을 저장할거에요 -> 속도를
_damage = damage; _damage = damage; // 값을 저장할거에요 -> 데미지를
_isInitialized = true; _isInitialized = true; // 상태를 바꿀거에요 -> 초기화 완료 상태를 참으로
// 속도가 있다? -> 화살/마법 (직선 운동) // 속도가 있다? -> 화살/마법 (직선 운동)
if (_rigidbody != null && speed > 0) if (_rigidbody != null && speed > 0) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있고 속도가 있다면
{ {
_rigidbody.velocity = _direction * _speed; _rigidbody.velocity = _direction * _speed; // 값을 넣을거에요 -> 속도 벡터를 리지드바디 속도에
_rigidbody.useGravity = false; // 마법은 보통 중력 무시 _rigidbody.useGravity = false; // 설정을 바꿀거에요 -> 중력 영향을 끄기로 (직선 비행)
} }
// 속도가 0이다? -> 돌맹이 (외부에서 물리력 가함) // 속도가 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인 경우 (강제 이동 보정) // 리지드바디가 없거나, 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. 적(몬스터)끼리 맞으면 무시 // 1. 적(몬스터)끼리 맞으면 무시
if (other.CompareTag("Enemy") || other.gameObject == gameObject) return; if (other.CompareTag("Enemy") || other.gameObject == gameObject) return; // 조건이 맞으면 중단할거에요 -> 적끼리 충돌했거나 자기 자신이라면
// 2. 투사체끼리 충돌 무시 // 2. 투사체끼리 충돌 무시
if (other.GetComponent<Projectile>()) return; if (other.GetComponent<Projectile>()) return; // 조건이 맞으면 중단할거에요 -> 다른 투사체와 충돌했다면
// 3. 플레이어 피격 처리 // 3. 플레이어 피격 처리
if (other.CompareTag("Player")) if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌 대상이 플레이어라면
{ {
// PlayerHealth 혹은 IDamageable 스크립트 찾기 // PlayerHealth 혹은 IDamageable 스크립트 찾기
var playerHealth = other.GetComponent<PlayerHealth>(); var playerHealth = other.GetComponent<PlayerHealth>(); // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를
if (playerHealth != null) if (playerHealth != null) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면
{ {
// 무적 상태 체크 (있다면) // 무적 상태 체크 (있다면)
if (!playerHealth.isInvincible) if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 플레이어가 무적 상태가 아니라면
{ {
playerHealth.TakeDamage(_damage); playerHealth.TakeDamage(_damage); // 함수를 실행할거에요 -> 데미지를 입히는 TakeDamage를
Debug.Log($"🎯 [적중] 플레이어에게 {_damage} 데미지!"); Debug.Log($"🎯 [적중] 플레이어에게 {_damage} 데미지!"); // 로그를 출력할거에요 -> 적중 메시지를
} }
} }
} }
// 4. 벽이나 땅에 닿았을 때도 이펙트 // 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); // 파괴할거에요 -> 이 투사체 오브젝트를
} }
} }
} }

View File

@ -1,77 +1,75 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
/// <summary> /// <summary>
/// 보스 카운터 시스템의 임계치, 가중치, 감소(Decay) 설정을 담는 ScriptableObject. /// 보스 카운터 시스템의 임계치, 가중치, 감소(Decay) 설정을 담는 ScriptableObject.
/// Inspector에서 밸런싱을 위해 쉽게 조정할 수 있습니다. /// Inspector에서 밸런싱을 위해 쉽게 조정할 수 있습니다.
///
/// [사용법] Project 창에서 우클릭 → Create → Boss/Counter Config
/// </summary> /// </summary>
[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/Counter Config")] [CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/Counter Config")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "Boss/Counter Config"를
public class BossCounterConfig : ScriptableObject public class BossCounterConfig : ScriptableObject // 클래스를 선언할거에요 -> ScriptableObject를 상속받는 BossCounterConfig를
{ {
[Header("══ 기본 임계치 (첫 런 / 잠금 해제 전) ══")] [Header("══ 기본 임계치 (첫 런 / 잠금 해제 전) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 기본 임계치 (첫 런 / 잠금 해제 전) ══ 을
[Tooltip("회피 카운터 발동 조건: 10초 내 회피 횟수")] [Tooltip("회피 카운터 발동 조건: 10초 내 회피 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public int dodgeThreshold = 5; public int dodgeThreshold = 5; // 변수를 선언할거에요 -> 회피 횟수 임계값(5)을 dodgeThreshold에
[Tooltip("조준 카운터 발동 조건: 10초 내 조준 유지 시간(초)")] [Tooltip("조준 카운터 발동 조건: 10초 내 조준 유지 시간(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public float aimThreshold = 4.0f; public float aimThreshold = 4.0f; // 변수를 선언할거에요 -> 조준 시간 임계값(4.0초)을 aimThreshold에
[Tooltip("관통 카운터 발동 조건: 10초 내 관통 비율")] [Tooltip("관통 카운터 발동 조건: 10초 내 관통 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[Range(0f, 1f)] [Range(0f, 1f)] // 슬라이더를 표시할거에요 -> 0부터 1 사이로
public float pierceThreshold = 0.6f; public float pierceThreshold = 0.6f; // 변수를 선언할거에요 -> 관통 비율 임계값(60%)을 pierceThreshold에
[Tooltip("관통 비율 판단을 위한 최소 발사 횟수")] [Tooltip("관통 비율 판단을 위한 최소 발사 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public int minShotsForPierceCheck = 3; public int minShotsForPierceCheck = 3; // 변수를 선언할거에요 -> 최소 발사 횟수(3)를 minShotsForPierceCheck에
[Header("══ 잠금 해제 후 임계치 감소 ══")] [Header("══ 잠금 해제 후 임계치 감소 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 잠금 해제 후 임계치 감소 ══ 를
[Tooltip("잠금 해제된 카운터의 임계치 감소 비율 (0.8 = 20% 낮아짐)")] [Tooltip("잠금 해제된 카운터의 임계치 감소 비율 (0.8 = 20% 낮아짐)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[Range(0.5f, 1.0f)] [Range(0.5f, 1.0f)] // 슬라이더를 표시할거에요 -> 0.5부터 1.0 사이로
public float unlockedThresholdMultiplier = 0.8f; public float unlockedThresholdMultiplier = 0.8f; // 변수를 선언할거에요 -> 잠금 해제 시 감소 배율(0.8)을 unlockedThresholdMultiplier에
[Header("══ 가중치 설정 (확률 가중치 방식) ══")] [Header("══ 가중치 설정 (확률 가중치 방식) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 가중치 설정 (확률 가중치 방식) ══ 을
[Tooltip("카운터 발동 시 해당 카운터 패턴에 추가되는 가중치")] [Tooltip("카운터 발동 시 해당 카운터 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public float counterWeightBonus = 3f; public float counterWeightBonus = 3f; // 변수를 선언할거에요 -> 카운터 패턴 보너스 가중치(3.0)를 counterWeightBonus에
[Tooltip("카운터 발동 시 보조 패턴에 추가되는 가중치")] [Tooltip("카운터 발동 시 보조 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public float counterSubWeightBonus = 2f; public float counterSubWeightBonus = 2f; // 변수를 선언할거에요 -> 보조 패턴 보너스 가중치(2.0)를 counterSubWeightBonus에
[Header("══ Decay(감소) 설정 ══")] [Header("══ Decay(감소) 설정 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ Decay(감소) 설정 ══ 을
[Tooltip("카운터 모드 유지 시간(초). 이 시간 동안 조건 미충족 시 비활성화")] [Tooltip("카운터 모드 유지 시간(초). 이 시간 동안 조건 미충족 시 비활성화")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public float counterDecayTime = 8f; public float counterDecayTime = 8f; // 변수를 선언할거에요 -> 카운터 유지 시간(8.0초)을 counterDecayTime에
[Header("══ 빈도 제한 ══")] [Header("══ 빈도 제한 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 빈도 제한 ══ 을
[Tooltip("카운터 패턴 선택 확률 상한 (0.25 = 최대 25%)")] [Tooltip("카운터 패턴 선택 확률 상한 (0.25 = 최대 25%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[Range(0.1f, 0.5f)] [Range(0.1f, 0.5f)] // 슬라이더를 표시할거에요 -> 0.1부터 0.5 사이로
public float maxCounterFrequency = 0.25f; public float maxCounterFrequency = 0.25f; // 변수를 선언할거에요 -> 카운터 최대 빈도(25%)를 maxCounterFrequency에
[Tooltip("카운터 패턴 발동 후 쿨타임(초)")] [Tooltip("카운터 패턴 발동 후 쿨타임(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public float counterCooldown = 5f; public float counterCooldown = 5f; // 변수를 선언할거에요 -> 카운터 재사용 대기시간(5.0초)을 counterCooldown에
[Header("══ 습관 변경 보상 ══")] [Header("══ 습관 변경 보상 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 습관 변경 보상 ══ 을
[Tooltip("플레이어가 이전 런에서 발동된 카운터 습관을 이번 런에서 안 보이면, 보스 일반 패턴 난이도 감소 비율")] [Tooltip("플레이어가 이전 런에서 발동된 카운터 습관을 이번 런에서 안 보이면, 보스 일반 패턴 난이도 감소 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[Range(0f, 0.3f)] [Range(0f, 0.3f)] // 슬라이더를 표시할거에요 -> 0부터 0.3 사이로
public float habitChangeRewardRatio = 0.15f; public float habitChangeRewardRatio = 0.15f; // 변수를 선언할거에요 -> 습관 변경 보상 비율(15%)을 habitChangeRewardRatio에
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// 임계치 조회 (잠금 해제 여부 반영) // 임계치 조회 (잠금 해제 여부 반영)
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
/// <summary>현재 유효 임계치 반환 (잠금 해제 시 낮아짐)</summary> /// <summary>현재 유효 임계치 반환 (잠금 해제 시 낮아짐)</summary>
public int GetEffectiveDodgeThreshold(bool isUnlocked) public int GetEffectiveDodgeThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 회피 임계값을 반환하는 GetEffectiveDodgeThreshold를
{ {
if (!isUnlocked) return dodgeThreshold; if (!isUnlocked) return dodgeThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
return Mathf.Max(2, Mathf.RoundToInt(dodgeThreshold * unlockedThresholdMultiplier)); return Mathf.Max(2, Mathf.RoundToInt(dodgeThreshold * unlockedThresholdMultiplier)); // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱하고 최소 2 이상이 되도록
} }
public float GetEffectiveAimThreshold(bool isUnlocked) public float GetEffectiveAimThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 조준 임계값을 반환하는 GetEffectiveAimThreshold를
{ {
if (!isUnlocked) return aimThreshold; if (!isUnlocked) return aimThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
return aimThreshold * unlockedThresholdMultiplier; return aimThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을
} }
public float GetEffectivePierceThreshold(bool isUnlocked) public float GetEffectivePierceThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 관통 임계값을 반환하는 GetEffectivePierceThreshold를
{ {
if (!isUnlocked) return pierceThreshold; if (!isUnlocked) return pierceThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
return pierceThreshold * unlockedThresholdMultiplier; return pierceThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을
} }
} }

View File

@ -1,48 +1,49 @@
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.UI; using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를
using TMPro; using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
/// <summary> /// <summary>
/// 보스 카운터 시스템의 연출/UI를 담당합니다. /// 보스 카운터 시스템의 연출/UI를 담당합니다.
/// 카운터 발동 시 보스 대사, 화면 효과 등을 처리합니다.
///
/// "...또 그 수법이냐" 같은 대사로 "보스가 기억한다"는 서사 전달.
/// </summary> /// </summary>
public class BossCounterFeedback : MonoBehaviour public class BossCounterFeedback : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterFeedback을
{ {
[Header("참조")] [Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를
[SerializeField] private BossCounterSystem counterSystem; [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 카운터 시스템 스크립트를 연결할 counterSystem을
[Header("UI 요소 (선택)")] [Header("UI 요소 (선택)")] // 인스펙터 창에 제목을 표시할거에요 -> UI 요소 (선택) 을
[SerializeField] private TextMeshProUGUI bossDialogueText; [SerializeField] private TextMeshProUGUI bossDialogueText; // 변수를 선언할거에요 -> 보스 대사를 표시할 텍스트 UI를
[SerializeField] private CanvasGroup dialogueCanvasGroup; [SerializeField] private CanvasGroup dialogueCanvasGroup; // 변수를 선언할거에요 -> 대사창의 투명도를 조절할 캔버스 그룹을
[SerializeField] private float dialogueDisplayDuration = 3f; [SerializeField] private float dialogueDisplayDuration = 3f; // 변수를 선언할거에요 -> 대사 표시 시간(3초)을 dialogueDisplayDuration에
[SerializeField] private float dialogueFadeDuration = 0.5f; [SerializeField] private float dialogueFadeDuration = 0.5f; // 변수를 선언할거에요 -> 대사 페이드 시간(0.5초)을 dialogueFadeDuration에
[Header("카운터별 보스 대사")] [Header("카운터별 보스 대사")] // 인스펙터 창에 제목을 표시할거에요 -> 카운터별 보스 대사 를
[SerializeField] private string[] dodgeCounterDialogues = new string[] [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("첫 잠금 해제 시 대사 (서사 강조)")] [Header("첫 잠금 해제 시 대사 (서사 강조)")] // 인스펙터 창에 제목을 표시할거에요 -> 첫 잠금 해제 시 대사 (서사 강조) 를
[SerializeField] private string[] firstUnlockDialogues = new string[] [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.OnCounterActivated.AddListener(OnCounterActivated); // 구독할거에요 -> 카운터 활성 이벤트에 OnCounterActivated를
counterSystem.OnCounterDeactivated.AddListener(OnCounterDeactivated); counterSystem.OnCounterDeactivated.AddListener(OnCounterDeactivated); // 구독할거에요 -> 카운터 비활성 이벤트에 OnCounterDeactivated를
counterSystem.OnHabitChangeRewarded.AddListener(OnHabitChangeRewarded); counterSystem.OnHabitChangeRewarded.AddListener(OnHabitChangeRewarded); // 구독할거에요 -> 보상 이벤트에 OnHabitChangeRewarded를
} }
} }
private void OnDisable() private void OnDisable() // 함수를 실행할거에요 -> 오브젝트 비활성화 시 호출되는 OnDisable을
{ {
if (counterSystem != null) if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면
{ {
counterSystem.OnCounterActivated.RemoveListener(OnCounterActivated); counterSystem.OnCounterActivated.RemoveListener(OnCounterActivated); // 구독 해제할거에요 -> 카운터 활성 이벤트를
counterSystem.OnCounterDeactivated.RemoveListener(OnCounterDeactivated); counterSystem.OnCounterDeactivated.RemoveListener(OnCounterDeactivated); // 구독 해제할거에요 -> 카운터 비활성 이벤트를
counterSystem.OnHabitChangeRewarded.RemoveListener(OnHabitChangeRewarded); 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이면 방금 처음 잠금 해제된 것) // 첫 잠금 해제인지 확인 (총 발동 횟수가 1이면 방금 처음 잠금 해제된 것)
bool isFirstUnlock = false; bool isFirstUnlock = false; // 변수를 초기화할거에요 -> 첫 해금 여부를 거짓(false)으로
if (persistence != null) if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면
{ {
int activations = type switch int activations = type switch // 값을 가져올거에요 -> 타입에 따른 발동 횟수를
{ {
CounterType.Dodge => persistence.Data.dodgeCounterActivations, CounterType.Dodge => persistence.Data.dodgeCounterActivations,
CounterType.Aim => persistence.Data.aimCounterActivations, CounterType.Aim => persistence.Data.aimCounterActivations,
CounterType.Pierce => persistence.Data.pierceCounterActivations, CounterType.Pierce => persistence.Data.pierceCounterActivations,
_ => 0 _ => 0
}; };
isFirstUnlock = (activations <= 1); isFirstUnlock = (activations <= 1); // 판단할거에요 -> 발동 횟수가 1 이하라면 첫 해금이라고
} }
// 대사 선택 // 대사 선택
string dialogue; string dialogue; // 변수를 선언할거에요 -> 출력할 대사를 담을 dialogue를
if (isFirstUnlock) 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.Dodge => dodgeCounterDialogues[Random.Range(0, dodgeCounterDialogues.Length)], // 선택할거에요 -> 회피 대사를
CounterType.Aim => aimCounterDialogues[Random.Range(0, aimCounterDialogues.Length)], CounterType.Aim => aimCounterDialogues[Random.Range(0, aimCounterDialogues.Length)], // 선택할거에요 -> 조준 대사를
CounterType.Pierce => pierceCounterDialogues[Random.Range(0, pierceCounterDialogues.Length)], CounterType.Pierce => pierceCounterDialogues[Random.Range(0, pierceCounterDialogues.Length)], // 선택할거에요 -> 관통 대사를
_ => "" _ => "" // 기본값은 빈 문자열로
}; };
} }
ShowDialogue(dialogue); ShowDialogue(dialogue); // 함수를 실행할거에요 -> 선택된 대사를 화면에 띄우는 ShowDialogue를
// TODO: 여기에 추가 연출 넣기 Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 재생" + (isFirstUnlock ? " (첫 잠금 해제!)" : "")); // 로그를 출력할거에요 -> 연출 재생 알림을
// - 보스 전용 애니메이션 트리거 (예: animator.SetTrigger("CounterReady"))
// - 보스 주변 이펙트 (파티클, 오라 등)
// - 카메라 연출 (줌인, 슬로모션 등)
// - 사운드 효과
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)]; string dialogue = habitChangeDialogues[Random.Range(0, habitChangeDialogues.Length)]; // 선택할거에요 -> 보상 대사 중 하나를 랜덤으로
ShowDialogue(dialogue); 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) if (dialogueCoroutine != null) // 조건이 맞으면 실행할거에요 -> 이미 실행 중인 대사가 있다면
StopCoroutine(dialogueCoroutine); 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; bossDialogueText.text = text; // 값을 설정할거에요 -> UI 텍스트 내용을 전달받은 text로
dialogueCanvasGroup.alpha = 0f; dialogueCanvasGroup.alpha = 0f; // 값을 설정할거에요 -> 투명도를 0(안 보임)으로
// 페이드 인 // 페이드 인
float t = 0f; float t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
while (t < dialogueFadeDuration) while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안
{ {
t += Time.deltaTime; t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을
dialogueCanvasGroup.alpha = t / dialogueFadeDuration; dialogueCanvasGroup.alpha = t / dialogueFadeDuration; // 값을 조절할거에요 -> 투명도를 서서히 1로
yield return null; yield return null; // 대기할거에요 -> 다음 프레임까지
} }
dialogueCanvasGroup.alpha = 1f; dialogueCanvasGroup.alpha = 1f; // 값을 확정할거에요 -> 투명도를 완전 불투명(1)으로
// 유지 // 유지
yield return new WaitForSeconds(dialogueDisplayDuration); yield return new WaitForSeconds(dialogueDisplayDuration); // 기다릴거에요 -> 대사 유지 시간만큼
// 페이드 아웃 // 페이드 아웃
t = 0f; t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 다시 0으로
while (t < dialogueFadeDuration) while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안
{ {
t += Time.deltaTime; t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을
dialogueCanvasGroup.alpha = 1f - (t / dialogueFadeDuration); dialogueCanvasGroup.alpha = 1f - (t / dialogueFadeDuration); // 값을 조절할거에요 -> 투명도를 서서히 0으로
yield return null; yield return null; // 대기할거에요 -> 다음 프레임까지
} }
dialogueCanvasGroup.alpha = 0f; dialogueCanvasGroup.alpha = 0f; // 값을 확정할거에요 -> 투명도를 완전 투명(0)으로
} }
} }

View File

@ -1,50 +1,40 @@
using System.Collections.Generic; using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
/// <summary> /// <summary>
/// 런 간 영구 저장되는 보스 카운터 잠금 해제 데이터. /// 런 간 영구 저장되는 보스 카운터 잠금 해제 데이터.
/// "보스가 플레이어를 기억한다"는 서사를 담당합니다.
///
/// 핵심 규칙:
/// - 잠금 해제는 영구 (런이 끝나도 유지)
/// - 발동은 조건부 (해당 런에서 플레이어가 습관을 보여야만 활성화)
/// - 잠금 해제된 카운터는 임계치가 약간 낮아짐 (보스가 "이미 알고 있으니 더 빨리 눈치챔")
/// </summary> /// </summary>
[System.Serializable] [System.Serializable] // 직렬화할거에요 -> 인스펙터나 JSON으로 저장 가능하게
public class BossCounterSaveData public class BossCounterSaveData // 클래스를 선언할거에요 -> 저장할 데이터 구조체인 BossCounterSaveData를
{ {
public bool dodgeCounterUnlocked = false; public bool dodgeCounterUnlocked = false; // 변수를 선언할거에요 -> 회피 카운터 해금 여부를
public bool aimCounterUnlocked = false; public bool aimCounterUnlocked = false; // 변수를 선언할거에요 -> 조준 카운터 해금 여부를
public bool pierceCounterUnlocked = false; public bool pierceCounterUnlocked = false; // 변수를 선언할거에요 -> 관통 카운터 해금 여부를
/// <summary>각 카운터가 발동된 총 횟수 (연출/난이도 스케일링에 활용 가능)</summary> /// <summary>각 카운터가 발동된 총 횟수</summary>
public int dodgeCounterActivations = 0; public int dodgeCounterActivations = 0; // 변수를 선언할거에요 -> 회피 카운터 총 발동 횟수를
public int aimCounterActivations = 0; public int aimCounterActivations = 0; // 변수를 선언할거에요 -> 조준 카운터 총 발동 횟수를
public int pierceCounterActivations = 0; public int pierceCounterActivations = 0; // 변수를 선언할거에요 -> 관통 카운터 총 발동 횟수를
} }
/// <summary> public class BossCounterPersistence : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterPersistence를
/// 보스 카운터 영구 데이터를 관리합니다.
/// PlayerPrefs 기반으로 런 간 저장/로드합니다.
/// </summary>
public class BossCounterPersistence : MonoBehaviour
{ {
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); Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를
return; return; // 중단할거에요 -> 초기화 로직을
} }
Instance = this; Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에
DontDestroyOnLoad(gameObject); DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬이 바뀌어도 이 오브젝트를 파괴하지 않게
Load(); Load(); // 함수를 실행할거에요 -> 저장된 데이터를 불러오는 Load를
} }
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
@ -52,88 +42,88 @@ public class BossCounterPersistence : MonoBehaviour
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
/// <summary>특정 카운터 타입을 영구 잠금 해제</summary> /// <summary>특정 카운터 타입을 영구 잠금 해제</summary>
public void UnlockCounter(CounterType type) public void UnlockCounter(CounterType type) // 함수를 선언할거에요 -> 카운터를 해금하는 UnlockCounter를
{ {
switch (type) switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라
{ {
case CounterType.Dodge: case CounterType.Dodge: // 조건이 맞으면 실행할거에요 -> 회피 타입이라면
if (!Data.dodgeCounterUnlocked) if (!Data.dodgeCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
{ {
Data.dodgeCounterUnlocked = true; Data.dodgeCounterUnlocked = true; // 값을 바꿀거에요 -> 회피 해금 상태를 참(true)으로
Debug.Log("[BossCounter] 회피 카운터 영구 잠금 해제!"); Debug.Log("[BossCounter] 회피 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
} }
break; break;
case CounterType.Aim: case CounterType.Aim: // 조건이 맞으면 실행할거에요 -> 조준 타입이라면
if (!Data.aimCounterUnlocked) if (!Data.aimCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
{ {
Data.aimCounterUnlocked = true; Data.aimCounterUnlocked = true; // 값을 바꿀거에요 -> 조준 해금 상태를 참(true)으로
Debug.Log("[BossCounter] 조준 카운터 영구 잠금 해제!"); Debug.Log("[BossCounter] 조준 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
} }
break; break;
case CounterType.Pierce: case CounterType.Pierce: // 조건이 맞으면 실행할거에요 -> 관통 타입이라면
if (!Data.pierceCounterUnlocked) if (!Data.pierceCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
{ {
Data.pierceCounterUnlocked = true; Data.pierceCounterUnlocked = true; // 값을 바꿀거에요 -> 관통 해금 상태를 참(true)으로
Debug.Log("[BossCounter] 관통 카운터 영구 잠금 해제!"); Debug.Log("[BossCounter] 관통 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
} }
break; break;
} }
Save(); Save(); // 함수를 실행할거에요 -> 변경된 데이터를 저장하는 Save를
} }
/// <summary>카운터가 잠금 해제되어 있는지 확인</summary> /// <summary>카운터가 잠금 해제되어 있는지 확인</summary>
public bool IsUnlocked(CounterType type) public bool IsUnlocked(CounterType type) // 함수를 선언할거에요 -> 해금 여부를 확인하는 IsUnlocked를
{ {
return type switch return type switch // 반환할거에요 -> 타입에 따른 해금 변수 값을
{ {
CounterType.Dodge => Data.dodgeCounterUnlocked, CounterType.Dodge => Data.dodgeCounterUnlocked, // 매칭되면 반환할거에요 -> 회피 해금 여부를
CounterType.Aim => Data.aimCounterUnlocked, CounterType.Aim => Data.aimCounterUnlocked, // 매칭되면 반환할거에요 -> 조준 해금 여부를
CounterType.Pierce => Data.pierceCounterUnlocked, CounterType.Pierce => Data.pierceCounterUnlocked, // 매칭되면 반환할거에요 -> 관통 해금 여부를
_ => false _ => false // 그 외에는 반환할거에요 -> 거짓(false)을
}; };
} }
/// <summary>카운터 발동 횟수 기록</summary> /// <summary>카운터 발동 횟수 기록</summary>
public void RecordActivation(CounterType type) public void RecordActivation(CounterType type) // 함수를 선언할거에요 -> 발동 횟수를 기록하는 RecordActivation을
{ {
switch (type) switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라
{ {
case CounterType.Dodge: Data.dodgeCounterActivations++; break; case CounterType.Dodge: Data.dodgeCounterActivations++; break; // 일치하면 실행할거에요 -> 회피 발동 횟수를 1 증가시키기를
case CounterType.Aim: Data.aimCounterActivations++; break; case CounterType.Aim: Data.aimCounterActivations++; break; // 일치하면 실행할거에요 -> 조준 발동 횟수를 1 증가시키기를
case CounterType.Pierce: Data.pierceCounterActivations++; break; case CounterType.Pierce: Data.pierceCounterActivations++; break; // 일치하면 실행할거에요 -> 관통 발동 횟수를 1 증가시키기를
} }
Save(); Save(); // 함수를 실행할거에요 -> 변경된 횟수를 저장하는 Save를
} }
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// 저장 / 로드 / 리셋 // 저장 / 로드 / 리셋
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
public void Save() public void Save() // 함수를 선언할거에요 -> 데이터를 디스크에 저장하는 Save를
{ {
string json = JsonUtility.ToJson(Data); string json = JsonUtility.ToJson(Data); // 변환할거에요 -> 데이터 객체를 JSON 문자열로
PlayerPrefs.SetString(SAVE_KEY, json); PlayerPrefs.SetString(SAVE_KEY, json); // 저장할거에요 -> JSON 문자열을 PlayerPrefs에
PlayerPrefs.Save(); 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); string json = PlayerPrefs.GetString(SAVE_KEY); // 불러올거에요 -> 저장된 JSON 문자열을
Data = JsonUtility.FromJson<BossCounterSaveData>(json); Data = JsonUtility.FromJson<BossCounterSaveData>(json); // 변환할거에요 -> JSON을 데이터 객체로 복구해서 Data에
} }
else else // 조건이 틀리면 실행할거에요 -> 저장된 데이터가 없다면
{ {
Data = new BossCounterSaveData(); Data = new BossCounterSaveData(); // 생성할거에요 -> 새로운 빈 데이터 객체를
} }
} }
/// <summary>모든 영구 데이터 초기화 (디버그/뉴게임+)</summary> /// <summary>모든 영구 데이터 초기화 (디버그/뉴게임+)</summary>
public void ResetAllData() public void ResetAllData() // 함수를 선언할거에요 -> 모든 데이터를 초기화하는 ResetAllData를
{ {
Data = new BossCounterSaveData(); Data = new BossCounterSaveData(); // 초기화할거에요 -> 데이터 객체를 새 것으로
PlayerPrefs.DeleteKey(SAVE_KEY); PlayerPrefs.DeleteKey(SAVE_KEY); // 삭제할거에요 -> 저장된 키를
Debug.Log("[BossCounter] 모든 보스 기억 데이터 초기화됨"); Debug.Log("[BossCounter] 모든 보스 기억 데이터 초기화됨"); // 로그를 출력할거에요 -> 초기화 완료 메시지를
} }
} }

View File

@ -1,213 +1,206 @@
using System.Collections.Generic; using System.Collections.Generic; // 리스트와 딕셔너리 기능을 사용할거에요 -> System.Collections.Generic을
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.Events; using UnityEngine.Events; // 유니티 이벤트 기능을 사용할거에요 -> UnityEngine.Events를
/// <summary> /// <summary>
/// 보스 카운터 시스템 메인 컨트롤러. /// 보스 카운터 시스템 메인 컨트롤러.
/// (수정됨: Config나 Player가 없어도 에러가 나지 않도록 안전장치가 추가된 버전) /// (수정됨: Config나 Player가 없어도 에러가 나지 않도록 안전장치가 추가된 버전)
///
/// 핵심 흐름:
/// 1. PlayerBehaviorTracker에서 실시간 행동 데이터를 읽음
/// 2. 임계치 판단 → 카운터 모드 ON/OFF (잠금 해제 여부에 따라 임계치 다름)
/// 3. 가중치 기반으로 보스 패턴 선택
/// 4. 첫 발동 시 해당 카운터를 영구 잠금 해제
/// 5. 플레이어가 습관을 바꾸면 보상 (난이도 완화)
/// </summary> /// </summary>
public class BossCounterSystem : MonoBehaviour public class BossCounterSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterSystem을
{ {
[Header("참조")] [Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를
[SerializeField] private BossCounterConfig config; [SerializeField] private BossCounterConfig config; // 변수를 선언할거에요 -> 카운터 설정 파일(BossCounterConfig)을 담을 config를
[Header("이벤트 (연출/UI 연동)")] [Header("이벤트 (연출/UI 연동)")] // 인스펙터 창에 제목을 표시할거에요 -> 이벤트 (연출/UI 연동) 을
[Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")] [Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public UnityEvent<CounterType> OnCounterActivated; public UnityEvent<CounterType> OnCounterActivated; // 이벤트를 선언할거에요 -> 카운터가 켜질 때 알릴 OnCounterActivated를
[Tooltip("카운터 모드가 비활성화될 때 발생")] [Tooltip("카운터 모드가 비활성화될 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public UnityEvent<CounterType> OnCounterDeactivated; public UnityEvent<CounterType> OnCounterDeactivated; // 이벤트를 선언할거에요 -> 카운터가 꺼질 때 알릴 OnCounterDeactivated를
[Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")] [Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public UnityEvent<string> OnCounterPatternSelected; public UnityEvent<string> OnCounterPatternSelected; // 이벤트를 선언할거에요 -> 패턴이 선택됐을 때 알릴 OnCounterPatternSelected를
[Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")] [Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public UnityEvent OnHabitChangeRewarded; public UnityEvent OnHabitChangeRewarded; // 이벤트를 선언할거에요 -> 습관 변경 보상을 알릴 OnHabitChangeRewarded를
// ── 카운터 모드 상태 ── // ── 카운터 모드 상태 ──
private Dictionary<CounterType, bool> activeCounters = new Dictionary<CounterType, bool>() private Dictionary<CounterType, bool> activeCounters = new Dictionary<CounterType, bool>() // 변수를 선언하고 초기화할거에요 -> 각 카운터 타입의 활성 상태를 저장할 딕셔너리 activeCounters를
{ {
{ CounterType.Dodge, false }, { CounterType.Dodge, false }, // 값을 넣을거에요 -> 회피 카운터 초기값을 거짓(false)으로
{ CounterType.Aim, false }, { CounterType.Aim, false }, // 값을 넣을거에요 -> 조준 카운터 초기값을 거짓(false)으로
{ CounterType.Pierce, false } { CounterType.Pierce, false } // 값을 넣을거에요 -> 관통 카운터 초기값을 거짓(false)으로
}; };
// ── Decay 타이머 ── // ── Decay 타이머 ──
private Dictionary<CounterType, float> counterTimers = new Dictionary<CounterType, float>() private Dictionary<CounterType, float> counterTimers = new Dictionary<CounterType, float>() // 변수를 선언하고 초기화할거에요 -> 각 카운터의 남은 지속 시간을 저장할 딕셔너리 counterTimers를
{ {
{ CounterType.Dodge, 0f }, { CounterType.Dodge, 0f }, // 값을 넣을거에요 -> 회피 카운터 타이머를 0으로
{ CounterType.Aim, 0f }, { CounterType.Aim, 0f }, // 값을 넣을거에요 -> 조준 카운터 타이머를 0으로
{ CounterType.Pierce, 0f } { CounterType.Pierce, 0f } // 값을 넣을거에요 -> 관통 카운터 타이머를 0으로
}; };
// ── 쿨타임 ── // ── 쿨타임 ──
private float lastCounterPatternTime = -100f; private float lastCounterPatternTime = -100f; // 변수를 선언할거에요 -> 마지막 카운터 패턴 사용 시간을 lastCounterPatternTime에 (초기값 -100)
// ── 습관 변경 보상 추적 ── // ── 습관 변경 보상 추적 ──
private HashSet<CounterType> previousRunCounters = new HashSet<CounterType>(); private HashSet<CounterType> previousRunCounters = new HashSet<CounterType>(); // 변수를 선언할거에요 -> 지난 런에서 당했던 카운터 목록을 previousRunCounters에
private HashSet<CounterType> currentRunActivatedCounters = new HashSet<CounterType>(); private HashSet<CounterType> currentRunActivatedCounters = new HashSet<CounterType>(); // 변수를 선언할거에요 -> 이번 런에서 발동된 카운터 목록을 currentRunActivatedCounters에
private bool habitChangeRewardGranted = false; private bool habitChangeRewardGranted = false; // 변수를 선언할거에요 -> 보상이 이미 지급되었는지 여부를 habitChangeRewardGranted에
// ── 보스 패턴 가중치 정의 ── // ── 보스 패턴 가중치 정의 ──
[System.Serializable] [System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정할 수 있게 BossPattern 클래스를
public class BossPattern public class BossPattern // 내부 클래스를 선언할거에요 -> 보스 패턴 정보를 담을 BossPattern을
{ {
public string patternName; public string patternName; // 변수를 선언할거에요 -> 패턴 이름을 저장할 patternName을
public float baseWeight = 1f; public float baseWeight = 1f; // 변수를 선언할거에요 -> 기본 가중치(확률)를 baseWeight에
[Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")] [Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public CounterType counterType = CounterType.None; public CounterType counterType = CounterType.None; // 변수를 선언할거에요 -> 이 패턴이 어떤 카운터 타입인지 지정할 counterType을
[Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")] [Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
public CounterType subCounterType = CounterType.None; public CounterType subCounterType = CounterType.None; // 변수를 선언할거에요 -> 보너스 가중치를 받을 보조 카운터 타입 subCounterType을
} }
[Header("보스 패턴 목록")] [Header("보스 패턴 목록")] // 인스펙터 창에 제목을 표시할거에요 -> 보스 패턴 목록 을
[SerializeField] private List<BossPattern> bossPatterns = new List<BossPattern>(); [SerializeField] private List<BossPattern> bossPatterns = new List<BossPattern>(); // 리스트를 선언할거에요 -> 보스의 모든 패턴 정보를 담을 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가 정상 작동하지 않을 수 있습니다."); // 경고 로그를 출력할거에요 -> 설정 파일 누락 경고를
} }
} }
/// <summary> /// <summary>
/// 보스 전투 시작 시 호출. 이전 런에서 발동된 카운터 정보를 기반으로 초기화. /// 보스 전투 시작 시 호출. 이전 런에서 발동된 카운터 정보를 기반으로 초기화.
/// </summary> /// </summary>
public void InitializeBattle() public void InitializeBattle() // 함수를 선언할거에요 -> 전투 시작 시 초기화를 담당할 InitializeBattle을
{ {
// 이전 런에서 잠금 해제된 카운터들을 기록 (습관 변경 보상 판단용) // 이전 런에서 잠금 해제된 카운터들을 기록 (습관 변경 보상 판단용)
previousRunCounters.Clear(); previousRunCounters.Clear(); // 비울거에요 -> 이전 런 카운터 목록을
currentRunActivatedCounters.Clear(); currentRunActivatedCounters.Clear(); // 비울거에요 -> 이번 런 카운터 목록을
habitChangeRewardGranted = false; habitChangeRewardGranted = false; // 초기화할거에요 -> 보상 지급 여부를 거짓(false)으로
var persistence = BossCounterPersistence.Instance; var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에
if (persistence != null) if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 존재한다면
{ {
if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge)) if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge)) // 조건이 맞으면 실행할거에요 -> 회피 카운터 기록이 있고 해제되었다면
previousRunCounters.Add(CounterType.Dodge); previousRunCounters.Add(CounterType.Dodge); // 추가할거에요 -> 회피 타입을 이전 런 목록에
if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim)) if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim)) // 조건이 맞으면 실행할거에요 -> 조준 카운터 기록이 있고 해제되었다면
previousRunCounters.Add(CounterType.Aim); previousRunCounters.Add(CounterType.Aim); // 추가할거에요 -> 조준 타입을 이전 런 목록에
if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce)) if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce)) // 조건이 맞으면 실행할거에요 -> 관통 카운터 기록이 있고 해제되었다면
previousRunCounters.Add(CounterType.Pierce); previousRunCounters.Add(CounterType.Pierce); // 추가할거에요 -> 관통 타입을 이전 런 목록에
} }
// 모든 카운터 모드 OFF // 모든 카운터 모드 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; activeCounters[type] = false; // 상태를 바꿀거에요 -> 해당 카운터를 비활성화(false)로
counterTimers[type] = 0f; 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 방지) // 🚨 [안전장치] 플레이어 트래커나 설정 파일이 없으면 실행 중지 (NullReferenceException 방지)
if (PlayerBehaviorTracker.Instance == null || config == null) return; if (PlayerBehaviorTracker.Instance == null || config == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 추적기나 설정 파일이 없다면
EvaluateCounters(); EvaluateCounters(); // 함수를 실행할거에요 -> 카운터 발동 조건을 검사하는 EvaluateCounters를
DecayCounters(); DecayCounters(); // 함수를 실행할거에요 -> 카운터 지속 시간을 관리하는 DecayCounters를
CheckHabitChangeReward(); CheckHabitChangeReward(); // 함수를 실행할거에요 -> 습관 변경 보상을 체크하는 CheckHabitChangeReward를
} }
/// <summary>플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단</summary> /// <summary>플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단</summary>
private void EvaluateCounters() private void EvaluateCounters() // 함수를 선언할거에요 -> 카운터 발동 여부를 판단하는 EvaluateCounters를
{ {
// 🚨 [안전장치] Config가 없으면 계산 불가 // 🚨 [안전장치] Config가 없으면 계산 불가
if (config == null) return; if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면
var tracker = PlayerBehaviorTracker.Instance; var tracker = PlayerBehaviorTracker.Instance; // 변수를 가져올거에요 -> 플레이어 행동 추적기를 tracker에
var persistence = BossCounterPersistence.Instance; var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에
// ── 회피 카운터 ── // ── 회피 카운터 ──
bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge); bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge); // 값을 확인할거에요 -> 회피 카운터가 해금되었는지 여부를
int dodgeThreshold = config.GetEffectiveDodgeThreshold(dodgeUnlocked); 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); bool aimUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Aim); // 값을 확인할거에요 -> 조준 카운터가 해금되었는지 여부를
float aimThreshold = config.GetEffectiveAimThreshold(aimUnlocked); 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); bool pierceUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Pierce); // 값을 확인할거에요 -> 관통 카운터가 해금되었는지 여부를
float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked); float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked); // 값을 가져올거에요 -> 현재 적용할 관통 임계값을
if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck // 조건이 맞으면 실행할거에요 -> 최소 발사 횟수를 충족하고
&& tracker.PierceRatio >= pierceThreshold) && tracker.PierceRatio >= pierceThreshold) // 조건이 맞으면 실행할거에요 -> 관통 공격 비율이 임계값 이상이라면
{ {
ActivateCounter(CounterType.Pierce); ActivateCounter(CounterType.Pierce); // 함수를 실행할거에요 -> 관통 카운터를 발동시키는 ActivateCounter를
} }
} }
private void ActivateCounter(CounterType type) private void ActivateCounter(CounterType type) // 함수를 선언할거에요 -> 특정 카운터를 활성화하는 ActivateCounter를
{ {
// 🚨 [안전장치] Config 확인 // 🚨 [안전장치] 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; activeCounters[type] = true; // 상태를 바꿀거에요 -> 카운터 활성 상태를 참(true)으로
currentRunActivatedCounters.Add(type); currentRunActivatedCounters.Add(type); // 추가할거에요 -> 이번 런 발동 목록에 해당 타입을
// 영구 잠금 해제 // 영구 잠금 해제
var persistence = BossCounterPersistence.Instance; var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에
if (persistence != null) if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면
{ {
persistence.UnlockCounter(type); persistence.UnlockCounter(type); // 함수를 실행할거에요 -> 해당 카운터를 영구 해금하는 UnlockCounter를
persistence.RecordActivation(type); persistence.RecordActivation(type); // 함수를 실행할거에요 -> 발동 횟수를 기록하는 RecordActivation을
} }
OnCounterActivated?.Invoke(type); OnCounterActivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 활성화 알림 이벤트를
Debug.Log($"[BossCounter] {type} 카운터 활성화!"); Debug.Log($"[BossCounter] {type} 카운터 활성화!"); // 로그를 출력할거에요 -> 카운터 활성화 메시지를
} }
} }
/// <summary>시간이 지나면 카운터 모드 자동 해제</summary> /// <summary>시간이 지나면 카운터 모드 자동 해제</summary>
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; counterTimers[type] -= Time.deltaTime; // 값을 뺄거에요 -> 남은 시간에서 프레임 시간을
if (counterTimers[type] <= 0f) if (counterTimers[type] <= 0f) // 조건이 맞으면 실행할거에요 -> 남은 시간이 0 이하라면
{ {
activeCounters[type] = false; activeCounters[type] = false; // 상태를 바꿀거에요 -> 카운터 활성 상태를 거짓(false)으로
OnCounterDeactivated?.Invoke(type); OnCounterDeactivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 비활성화 알림 이벤트를
Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화"); Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화"); // 로그를 출력할거에요 -> 시간 만료로 인한 비활성화 메시지를
} }
} }
} }
@ -220,25 +213,25 @@ public class BossCounterSystem : MonoBehaviour
/// 이전 런에서 카운터 당한 습관을 이번 런에서 보이지 않으면 보상. /// 이전 런에서 카운터 당한 습관을 이번 런에서 보이지 않으면 보상.
/// 전투 중반(30초 이후) 한 번 체크. /// 전투 중반(30초 이후) 한 번 체크.
/// </summary> /// </summary>
private float battleTimer = 0f; private float battleTimer = 0f; // 변수를 초기화할거에요 -> 전투 진행 시간을 0으로
private const float HABIT_CHECK_TIME = 30f; 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; battleTimer += Time.deltaTime; // 값을 더할거에요 -> 전투 진행 시간에 프레임 시간을
if (battleTimer < HABIT_CHECK_TIME) return; 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; habitChangeRewardGranted = true; // 상태를 바꿀거에요 -> 보상 지급 여부를 참(true)으로
OnHabitChangeRewarded?.Invoke(); OnHabitChangeRewarded?.Invoke(); // 이벤트를 실행할거에요 -> 보상 지급 알림 이벤트를
Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여"); Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여"); // 로그를 출력할거에요 -> 보상 부여 메시지를
break; break; // 중단할거에요 -> 보상은 한 번만 주므로 반복문을
} }
} }
} }
@ -252,106 +245,106 @@ public class BossCounterSystem : MonoBehaviour
/// 보스 AI의 패턴 선택 시점에 호출하세요. /// 보스 AI의 패턴 선택 시점에 호출하세요.
/// </summary> /// </summary>
/// <returns>선택된 패턴 이름</returns> /// <returns>선택된 패턴 이름</returns>
public string SelectBossPattern() public string SelectBossPattern() // 함수를 선언할거에요 -> 보스 패턴을 선택해서 반환하는 SelectBossPattern을
{ {
if (bossPatterns.Count == 0) if (bossPatterns.Count == 0) // 조건이 맞으면 실행할거에요 -> 등록된 패턴이 하나도 없다면
{ {
Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!"); Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!"); // 경고 로그를 출력할거에요 -> 패턴 없음 경고를
return "Normal"; // 기본값 리턴 return "Normal"; // 값을 반환할거에요 -> 기본값 "Normal"을
} }
// 🚨 [안전장치] Config가 없으면 그냥 랜덤 패턴 반환 (멈춤 방지) // 🚨 [안전장치] Config가 없으면 그냥 랜덤 패턴 반환 (멈춤 방지)
if (config == null) if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일이 없다면
{ {
int randomIndex = Random.Range(0, bossPatterns.Count); int randomIndex = Random.Range(0, bossPatterns.Count); // 값을 랜덤으로 뽑을거에요 -> 0부터 패턴 개수 사이의 인덱스를
return bossPatterns[randomIndex].patternName; return bossPatterns[randomIndex].patternName; // 값을 반환할거에요 -> 랜덤하게 뽑힌 패턴 이름을
} }
// 가중치 계산 // 가중치 계산
float[] weights = new float[bossPatterns.Count]; float[] weights = new float[bossPatterns.Count]; // 배열을 만들거에요 -> 각 패턴의 가중치를 담을 weights 배열을
float totalWeight = 0f; float totalWeight = 0f; // 변수를 초기화할거에요 -> 전체 가중치 합계를 0으로
float counterWeight = 0f; 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 if (bossPatterns[i].counterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 카운터 타입이 설정되어 있고
&& activeCounters.ContainsKey(bossPatterns[i].counterType) && activeCounters.ContainsKey(bossPatterns[i].counterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며
&& activeCounters[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 if (bossPatterns[i].subCounterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 보조 카운터 타입이 설정되어 있고
&& activeCounters.ContainsKey(bossPatterns[i].subCounterType) && activeCounters.ContainsKey(bossPatterns[i].subCounterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며
&& activeCounters[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) if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면
counterWeight += weights[i]; counterWeight += weights[i]; // 값을 더할거에요 -> 카운터 가중치 합계에
} }
// 빈도 상한 체크: 카운터 패턴 총 비율이 maxCounterFrequency를 넘지 않도록 // 빈도 상한 체크: 카운터 패턴 총 비율이 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 allowedCounterWeight = (totalWeight - counterWeight) * config.maxCounterFrequency / (1f - config.maxCounterFrequency); // 값을 계산할거에요 -> 허용 가능한 카운터 총 가중치를
float scale = allowedCounterWeight / counterWeight; 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; float bonus = weights[i] - bossPatterns[i].baseWeight; // 값을 계산할거에요 -> 추가된 보너스 가중치를
weights[i] = bossPatterns[i].baseWeight + bonus * scale; weights[i] = bossPatterns[i].baseWeight + bonus * scale; // 값을 수정할거에요 -> 보너스를 비율대로 줄여서 다시 적용
} }
} }
// 총 가중치 재계산 // 총 가중치 재계산
totalWeight = 0f; totalWeight = 0f; // 값을 초기화할거에요 -> 전체 합계를 0으로
for (int i = 0; i < weights.Length; i++) for (int i = 0; i < weights.Length; i++) // 반복할거에요 -> 모든 가중치에 대해
totalWeight += weights[i]; totalWeight += weights[i]; // 값을 더할거에요 -> 전체 합계에
} }
// 가중치 랜덤 선택 // 가중치 랜덤 선택
float roll = Random.Range(0f, totalWeight); float roll = Random.Range(0f, totalWeight); // 값을 랜덤으로 뽑을거에요 -> 0부터 전체 가중치 사이의 값을 roll에
float cumulative = 0f; float cumulative = 0f; // 변수를 초기화할거에요 -> 누적 가중치를 0으로
for (int i = 0; i < bossPatterns.Count; i++) for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해
{ {
cumulative += weights[i]; cumulative += weights[i]; // 값을 더할거에요 -> 누적 가중치에 현재 가중치를
if (roll <= cumulative) 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); OnCounterPatternSelected?.Invoke(selected); // 이벤트를 실행할거에요 -> 패턴 선택 알림 이벤트를
return selected; return selected; // 값을 반환할거에요 -> 선택된 패턴 이름을
} }
} }
return bossPatterns[bossPatterns.Count - 1].patternName; return bossPatterns[bossPatterns.Count - 1].patternName; // 값을 반환할거에요 -> 만약 선택되지 않았다면 마지막 패턴을 (안전장치)
} }
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
@ -359,22 +352,22 @@ public class BossCounterSystem : MonoBehaviour
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
/// <summary>특정 카운터 모드가 현재 활성 상태인지 확인</summary> /// <summary>특정 카운터 모드가 현재 활성 상태인지 확인</summary>
public bool IsCounterActive(CounterType type) public bool IsCounterActive(CounterType type) // 함수를 선언할거에요 -> 특정 카운터 활성 여부를 반환하는 IsCounterActive를
{ {
return activeCounters.ContainsKey(type) && activeCounters[type]; return activeCounters.ContainsKey(type) && activeCounters[type]; // 값을 반환할거에요 -> 딕셔너리에 키가 있고 값이 참(true)인지를
} }
/// <summary>현재 활성화된 모든 카운터 타입 반환</summary> /// <summary>현재 활성화된 모든 카운터 타입 반환</summary>
public List<CounterType> GetActiveCounters() public List<CounterType> GetActiveCounters() // 함수를 선언할거에요 -> 활성화된 모든 카운터 목록을 반환하는 GetActiveCounters를
{ {
var result = new List<CounterType>(); var result = new List<CounterType>(); // 리스트를 만들거에요 -> 결과를 담을 result 리스트를
foreach (var kvp in activeCounters) foreach (var kvp in activeCounters) // 반복할거에요 -> 모든 카운터 상태에 대해
{ {
if (kvp.Value) result.Add(kvp.Key); if (kvp.Value) result.Add(kvp.Key); // 조건이 맞으면 실행할거에요 -> 활성화(true) 상태라면 리스트에 추가하기를
} }
return result; return result; // 값을 반환할거에요 -> 완성된 리스트를
} }
/// <summary>습관 변경 보상이 활성화되었는지 확인</summary> /// <summary>습관 변경 보상이 활성화되었는지 확인</summary>
public bool IsHabitChangeRewarded => habitChangeRewardGranted; public bool IsHabitChangeRewarded => habitChangeRewardGranted; // 프로퍼티를 선언할거에요 -> 보상 지급 여부를 외부에서 읽을 수 있는 IsHabitChangeRewarded를
} }

View File

@ -1,460 +1,443 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
public class NorcielBoss : MonsterClass public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를
{ {
[Header("--- 🧠 두뇌 연결 ---")] [Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🧠 두뇌 연결 ---을
[SerializeField] private BossCounterSystem counterSystem; [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 보스의 패턴을 결정할 counterSystem을
[Header("--- ⚔️ 패턴 설정 ---")] [Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- ⚔️ 패턴 설정 ---을
[SerializeField] private float patternInterval = 3f; [SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 대기 시간을 3초로 지정하는 patternInterval을
[SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 이 거리 안에 플레이어가 들어오면 공격하는 attackRange를 3으로
[Header("--- 🎱 무기(쇠공) 설정 (필수!) ---")] [Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🎱 무기 설정 ---을
[SerializeField] private GameObject ironBall; [SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 던지고 주울 무기 오브젝트인 ironBall을
[SerializeField] private Transform handHolder; [SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 무기를 잡을 손의 위치인 handHolder를
[Header("--- 📊 UI 연결 ---")] [Header("--- 📊 UI 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 📊 UI 연결 ---을
[SerializeField] private GameObject bossHealthBar; [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 float _timer; // 변수를 선언할거에요 -> 공격 쿨타임을 계산할 내부 타이머 _timer를
private Rigidbody rb; private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 엔진을 제어할 rb를
private Rigidbody ballRb; private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 엔진을 제어할 ballRb를
private bool isBattleStarted = false; private bool isBattleStarted = false; // 변수를 초기화할거에요 -> 전투 시작 상태를 거짓(false)으로
private bool isWeaponless = false; private bool isWeaponless = false; // 변수를 초기화할거에요 -> 맨손 상태를 거짓(false)으로
private bool isPerformingAction = false; // ⭐ 신규: 연출 중 플래그 (Roar 등) private bool isPerformingAction = false; // 변수를 초기화할거에요 -> 연출 진행 상태를 거짓(false)으로
private Transform target; private Transform target; // 변수를 선언할거에요 -> 보스가 쫓아갈 대상의 위치인 target을
protected override void Awake() protected override void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를
{ {
base.Awake(); base.Awake(); // 부모 클래스의 함수를 먼저 실행할거에요 -> MonsterClass의 Awake 로직을
rb = GetComponent<Rigidbody>(); rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져와서 저장할거에요 -> 보스 자신의 Rigidbody를 rb에
if (ironBall != null) ballRb = ironBall.GetComponent<Rigidbody>(); if (ironBall != null) ballRb = ironBall.GetComponent<Rigidbody>(); // 조건이 맞으면 가져올거에요 -> ironBall이 있다면 그것의 Rigidbody를 ballRb에
} }
// ⭐ [수정] Start → OnEnable 이후 호출되도록 변경 private void Start() // 함수를 실행할거에요 -> 첫 프레임이 시작될 때 호출되는 Start를
private void Start()
{ {
StartBossBattle(); StartBossBattle(); // 함수를 실행할거에요 -> Roar 연출 후 전투를 시작하는 StartBossBattle을
} }
protected override void Init() protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 몬스터 초기화를 담당하는 Init을
{ {
_timer = patternInterval; base.Init(); // 부모 초기화 호출
isBattleStarted = false; _timer = 0f; // 값을 넣을거에요 -> 전투 시작하자마자 첫 공격이 가능하게 타이머를 0으로
isWeaponless = false; isBattleStarted = false; // 상태를 바꿀거에요 -> 전투 시작 상태를 거짓(false)으로
isPerformingAction = false; isWeaponless = false; // 상태를 바꿀거에요 -> 맨손 상태를 거짓(false)으로
isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 상태를 거짓(false)으로
// 플레이어 찾기 IsAggroed = true; // 상태를 바꿀거에요 -> 부모 클래스의 영구 어그로 상태를 참(true)으로
GameObject playerObj = GameObject.FindWithTag("Player"); mobRenderer = null; // 값을 지울거에요 -> 화면 밖 최적화를 끄기 위해 렌더러를 null로
if (playerObj != null) target = playerObj.transform; optimizationDistance = 9999f; // 값을 바꿀거에요 -> 거리가 멀어져도 멈추지 않게 최적화 거리를 9999로
else
GameObject playerObj = GameObject.FindWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그가 붙은 녀석을
if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 태그로 찾은 플레이어가 존재한다면
{ {
var playerScript = FindObjectOfType<PlayerMovement>(); target = playerObj.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에
if (playerScript != null) target = playerScript.transform; playerTransform = playerObj.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를
}
else // 조건이 틀리면 실행할거에요 -> 태그로 찾지 못했다면
{
var playerScript = FindObjectOfType<PlayerMovement>(); // 컴포넌트를 찾을거에요 -> PlayerMovement 스크립트를 가진 오브젝트를
if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 스크립트로 플레이어를 찾았다면
{
target = playerScript.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에
playerTransform = playerScript.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를
}
} }
// HP 초기화 if (target == null) // 조건이 맞으면 실행할거에요 -> 어떤 방법으로도 플레이어를 못 찾았다면
currentHP = maxHP; Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 출력할거에요 -> 플레이어 없음 경고 메시지를
StartCoroutine(SafeInitNavMesh()); currentHP = maxHP; // 체력을 채울거에요 -> 현재 체력을 최대 체력으로
StartCoroutine(SafeInitNavMesh()); // 코루틴을 실행할거에요 -> 길찾기를 안전하게 켜는 SafeInitNavMesh를
if (bossHealthBar != null) bossHealthBar.SetActive(false); if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를
if (ballRb != null) ballRb.isKinematic = true; if (ballRb != null) ballRb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 쇠공 물리가 있다면 중력 연산을 끄기를
if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 대기 모션을
} }
private IEnumerator SafeInitNavMesh() private IEnumerator SafeInitNavMesh() // 코루틴 함수를 선언할거에요 -> 길찾기를 안전하게 초기화하는 SafeInitNavMesh를
{ {
yield return null; yield return null; // 기다릴거에요 -> 다음 프레임이 될 때까지 한 턴을
if (agent != null) // 조건이 맞으면 실행할거에요 -> NavMeshAgent가 존재한다면
if (agent != null)
{ {
int retry = 0; int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로
while (!agent.isOnNavMesh && retry < 5) while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 바닥에 없고 시도가 5번 미만인 동안
{ {
retry++; retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼
yield return new WaitForSeconds(0.1f); yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을
}
if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 바닥에 닿았다면 이동을 정지로
agent.enabled = false; // 기능을 끌거에요 -> 전투 시작 전까지 NavMeshAgent를 비활성화로
}
} }
if (agent.isOnNavMesh) protected override void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
{ {
agent.isStopped = true; base.Update(); // 부모 클래스의 Update도 호출 (최적화 로직 등)
}
agent.enabled = false; if (!isDead) // 조건이 맞으면 실행할거에요 -> 보스가 살아있다면
{
ExecuteAILogic(); // 함수를 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을
} }
} }
// ════════════════════════════════════════ // ════════════════════════════════════════
// ⭐ [핵심 수정] 부모의 OnManagedUpdate 흐름 제어 // 🧠 AI 핵심 로직
// ════════════════════════════════════════ // ════════════════════════════════════════
/// <summary> protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을
/// 부모 MonsterClass의 StopMovement()가 매 프레임 Monster_Idle을
/// 강제 재생해서 Roar/다른 애니메이션을 덮어쓰는 문제 해결.
///
/// 보스는 일반 몬스터와 다르게:
/// - 전투 시작 전에는 아무 AI도 돌리지 않음
/// - 연출 중(Roar 등)에는 애니메이션을 건드리지 않음
/// </summary>
protected override void ExecuteAILogic()
{ {
// 연출 중이거나 전투 미시작 → 아무것도 안 함 if (isPerformingAction || !isBattleStarted || target == null) return; // 조건이 맞으면 중단할거에요 -> 연출중, 전투전, 타겟없음 중 하나라도 해당되면
if (isPerformingAction || !isBattleStarted || target == null) return;
// 무기 없으면 회수 우선 if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 쇠공을 던져서 맨손이라면
if (isWeaponless)
{ {
RetrieveWeaponLogic(); RetrieveWeaponLogic(); // 함수를 실행할거에요 -> 쇠공을 주우러 가는 로직을
return; return; // 중단할거에요 -> 무기 줍기가 우선이므로 아래 로직을
} }
// --- 일반 전투 로직 --- if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 AI 판단을
_timer -= Time.deltaTime;
if (_timer <= 0 && !isAttacking && !isHit && !isDead && !isResting) if (_timer > 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 남아있다면
{ {
_timer = patternInterval; _timer -= Time.deltaTime; // 값을 뺄거에요 -> 쿨타임 타이머에서 지난 프레임 시간만큼을
DecideAttack();
} }
// 이동 (공격/피격 중이 아닐 때만) float distToPlayer = Vector3.Distance(transform.position, target.position); // 거리를 계산할거에요 -> 보스와 플레이어 사이의 거리를
if (!isAttacking && !isHit && !isResting && agent != null && agent.enabled && agent.isOnNavMesh)
{
agent.SetDestination(target.position);
if (animator != null) animator.SetFloat("Speed", agent.velocity.magnitude); if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 길찾기가 불가능한 상태라면
if (agent.remainingDistance <= agent.stoppingDistance + 0.5f) if (distToPlayer > attackRange) // 조건이 맞으면 실행할거에요 -> 플레이어가 공격 사거리 밖이라면
{ {
agent.isStopped = true; agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고
LookAtTarget(target.position); agent.SetDestination(target.position); // 명령을 내릴거에요 -> 길찾기 목표를 플레이어 위치로
if (animator != null) animator.SetFloat("Speed", 0f); LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걷기 애니메이션을 맞추는 UpdateMovementAnimation을
} }
else else // 조건이 틀리면 실행할거에요 -> 플레이어가 사거리 안에 있다면
{ {
agent.isStopped = false; agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
} agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 완전한 0으로
} LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을
}
// ════════════════════════════════════════ if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 존재한다면
// 🏃‍♂️ 무기 회수 로직
// ════════════════════════════════════════
private void RetrieveWeaponLogic()
{ {
if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를
if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷기 모션이 재생 중이라면
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()); animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로
} }
} }
private IEnumerator PickUpBallRoutine() if (_timer <= 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 0 이하라면
{ {
OnAttackStart(); _timer = patternInterval; // 값을 넣을거에요 -> 쿨타임을 다시 패턴 대기 시간으로
Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 출력할거에요 -> 공격 시점과 거리 정보를
if (agent != null && agent.isOnNavMesh) DecideAttack(); // 함수를 실행할거에요 -> 어떤 공격을 할지 고르는 DecideAttack을
{
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; if (isBattleStarted) return; // 조건이 맞으면 중단할거에요 -> 이미 전투가 시작되었다면 중복 실행을
StartCoroutine(BattleStartRoutine()); 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이 아무것도 안 함 if (bossHealthBar != null) bossHealthBar.SetActive(true); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에 켜기를
isPerformingAction = true; if (counterSystem != null) counterSystem.InitializeBattle(); // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 있다면 전투 초기화를
if (bossHealthBar != null) bossHealthBar.SetActive(true); if (animator != null) animator.Play(anim_Roar, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar 애니메이션을 처음부터 재생을
if (counterSystem != null) counterSystem.InitializeBattle();
// ⭐ [핵심] Roar 재생 yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> Roar 애니메이션이 끝날 2초의 시간을
if (animator != null)
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); agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를 활성화(true)로
int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로
while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> NavMesh에 안 닿았고 10번 미만인 동안
{
retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을
} }
// Roar 애니메이션 대기 (길이에 맞게 조절) if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 올라갔다면
yield return new WaitForSeconds(2.0f);
// ⭐ [핵심] Roar 끝나면 Idle로 전환
if (animator != null)
{ {
animator.Play("Monster_Idle", 0, 0f); agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 추격을 시작하라고
Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 출력할거에요 -> NavMesh 연결 성공 메시지를
} }
else // 조건이 틀리면 실행할거에요 -> NavMesh에 못 올라갔다면
// 연출 종료 → 전투 시작
isPerformingAction = false;
isBattleStarted = true;
// NavMeshAgent 활성화
if (agent != null)
{ {
agent.enabled = true; Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 출력할거에요 -> 연결 실패 경고를
// NavMesh 위에 올라갈 때까지 잠시 대기
int retry = 0;
while (!agent.isOnNavMesh && retry < 10)
{
retry++;
yield return new WaitForSeconds(0.1f);
} }
if (agent.isOnNavMesh)
{
agent.isStopped = false;
} }
} }
Debug.Log("⚔️ 보스 전투 루프 시작!"); // ════════════════════════════════════════
// 🚶 이동 애니메이션 & 타겟팅
// ════════════════════════════════════════
private void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 상태에 맞는 애니메이션을 재생하는 UpdateMovementAnimation을
{
if (animator == null || isPerformingAction || isAttacking || isHit || isDead) return; // 조건이 맞으면 중단할거에요 -> 애니메이션을 바꿀 수 없는 상태라면
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를
if (agent != null && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 현재 이동 속도가 0.1보다 크다면
{
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) private void LookAtTarget(Vector3 targetPos) // 함수를 선언할거에요 -> 타겟을 부드럽게 쳐다보는 LookAtTarget을
{ {
Vector3 dir = targetPos - transform.position; Vector3 dir = targetPos - transform.position; // 벡터를 계산할거에요 -> 내 위치에서 타겟까지의 방향을
dir.y = 0; dir.y = 0; // 값을 바꿀거에요 -> 위아래로 고개가 꺾이지 않게 y를 0으로
if (dir != Vector3.zero) if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 0이 아니라면
{ {
transform.rotation = Quaternion.Slerp( transform.rotation = Quaternion.Slerp( // 회전시킬거에요 -> 현재 회전에서 타겟 방향으로 부드럽게
transform.rotation, transform.rotation, // 시작값으로 사용할거에요 -> 현재 회전값을
Quaternion.LookRotation(dir), Quaternion.LookRotation(dir), // 목표값으로 사용할거에요 -> 타겟을 향하는 회전값을
Time.deltaTime * 5f 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); // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 그쪽을 바라보기를
// 공격 전 이동 멈춤 string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 변수를 선언할거에요 -> 카운터 시스템에서 받은 패턴 이름을 patternName에
if (agent != null && agent.isOnNavMesh) Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 출력할거에요 -> 선택된 패턴 이름을
switch (patternName) // 분기할거에요 -> 받아온 패턴 이름에 따라서
{ {
agent.isStopped = true; case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 일치하면 실행할거에요 -> 돌진 공격을
agent.velocity = Vector3.zero; case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 일치하면 실행할거에요 -> 내려찍기 공격을
} case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 일치하면 실행할거에요 -> 휩쓸기 공격을
default: StartCoroutine(Pattern_ThrowBall()); break; // 맞는게 없으면 실행할거에요 -> 공 던지기 공격을
// 플레이어를 바라봄
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;
} }
} }
// ════════════════════════════════════════ // ════════════════════════════════════════
// ⚔️ 공격 패턴 // 🏃 무기 회수
// ════════════════════════════════════════ // ════════════════════════════════════════
// 1. 돌진 private void RetrieveWeaponLogic() // 함수를 선언할거에요 -> 던진 쇠공을 주우러 가는 RetrieveWeaponLogic을
private IEnumerator Pattern_DashAttack()
{ {
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); if (Vector3.Distance(transform.position, ironBall.transform.position) <= 3.0f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 쇠공과 3m 이내이고 공격 중이 아니라면
yield return new WaitForSeconds(0.5f); StartCoroutine(PickUpBallRoutine()); // 코루틴을 실행할거에요 -> 바닥의 쇠공을 줍는 PickUpBallRoutine을
// 돌진
if (rb != null)
{
rb.isKinematic = false;
rb.velocity = transform.forward * 20f;
} }
if (animator != null) animator.Play("Skill_Dash_Go", 0, 0f); private IEnumerator PickUpBallRoutine() // 코루틴 함수를 선언할거에요 -> 쇠공 줍기 행동인 PickUpBallRoutine을
yield return new WaitForSeconds(1.0f);
// 정지
if (rb != null)
{ {
rb.velocity = Vector3.zero; OnAttackStart(); // 상태를 켤거에요 -> 다른 행동과 겹치지 않게 공격 플래그를
rb.isKinematic = true; 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초를
// NavMeshAgent 복구 ironBall.transform.SetParent(handHolder); // 부모를 바꿀거에요 -> 쇠공을 보스의 손 밑으로
if (agent != null && isBattleStarted) ironBall.transform.localPosition = Vector3.zero; // 위치를 바꿀거에요 -> 쇠공의 위치를 손의 정중앙으로
{ ironBall.transform.localRotation = Quaternion.identity; // 회전을 바꿀거에요 -> 쇠공의 회전을 기본값으로
agent.enabled = true; if (ballRb != null) { ballRb.isKinematic = true; ballRb.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 쇠공의 물리를 끄고 속도를 0으로
// NavMesh 복귀 대기
int retry = 0;
while (agent != null && !agent.isOnNavMesh && retry < 10)
{
retry++;
yield return new WaitForSeconds(0.1f);
}
}
OnAttackEnd(); yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 일어서는 애니메이션이 끝날 1초를
} isWeaponless = false; // 상태를 바꿀거에요 -> 무기를 다시 쥐었으므로 맨손 상태를 거짓으로
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 처리를
// 2. 스매시 if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 다시 추격을 위해 이동 정지 해제를
private IEnumerator Pattern_SmashAttack()
{
OnAttackStart();
if (animator != null) animator.Play("Skill_Smash_Impact", 0, 0f);
yield return new WaitForSeconds(1.2f);
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();
} }
// ════════════════════════════════════════ // ════════════════════════════════════════
// ⭐ [신규] 피격 시 부모 클래스 StopAllCoroutines 대응 // ✅ 공격 시작/종료 처리 (핵심)
// ════════════════════════════════════════ // ════════════════════════════════════════
/// <summary> public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 처리를 하는 OnAttackStart를
/// 부모의 StartHit()이 StopAllCoroutines()를 호출하므로,
/// 보스 전용 상태를 여기서 복구합니다.
/// </summary>
protected override void OnStartHit()
{ {
isPerformingAction = false; // 연출 중이었으면 해제 isAttacking = true; // 상태를 바꿀거에요 -> 공격 중임을 참으로
isPerformingAction = true; // 상태를 바꿀거에요 -> 다른 행동 불가 상태로
// 공격 중이었으면 해제 if (agent != null) agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
if (isAttacking)
{
isAttacking = false;
if (myWeapon != null) myWeapon.DisableHitBox();
} }
// 돌진 중 물리 복구 public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를
if (rb != null && !rb.isKinematic)
{ {
rb.velocity = Vector3.zero; StartCoroutine(RecoverRoutine()); // 코루틴을 시작할거에요 -> 후딜레이(회복) 처리를 위한 RecoverRoutine을
rb.isKinematic = true;
}
} }
/// <summary> private IEnumerator RecoverRoutine() // 코루틴 함수를 선언할거에요 -> 공격 후 대기 시간을 갖는 RecoverRoutine을
/// 피격 종료 후 NavMeshAgent 복구
/// </summary>
public override void OnHitEnd()
{ {
base.OnHitEnd(); isAttacking = false; // 상태를 바꿀거에요 -> 공격이 끝났으므로 공격 중 상태를 거짓으로
isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참으로 (이때는 추격 안 함)
// 전투 중이면 Agent 다시 활성화 if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 대기 모션으로 전환을
if (isBattleStarted && agent != null && !agent.enabled)
{ // 딜레이 시간 (여기서 patternInterval 만큼 멍때림)
agent.enabled = true; yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 패턴 간격 시간만큼
}
isResting = false; // 상태를 바꿀거에요 -> 휴식이 끝났으므로 거짓으로
isPerformingAction = false; // 상태를 바꿀거에요 -> 이제 다시 다른 행동이 가능하도록 거짓으로
} }
// ════════════════════════════════════════ // ════════════════════════════════════════
// ⭐ [신규] 사망 처리 // ⚔️ 공격 패턴들
// ════════════════════════════════════════ // ════════════════════════════════════════
protected override void OnDie() // 패턴 1: 돌진 공격
private IEnumerator Pattern_DashAttack() // 코루틴 함수를 선언할거에요 -> 돌진 패턴인 Pattern_DashAttack을
{ {
isBattleStarted = false; OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
isPerformingAction = false; if (agent != null) agent.enabled = false; // 조건이 맞으면 실행할거에요 -> 돌진 시 물리력을 위해 길찾기를 끄기로
isWeaponless = false;
// 물리 정리 if (animator != null) animator.Play(anim_DashReady, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 준비 모션을
if (rb != null) yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 준비 포즈를 취할 0.5초를
{
rb.velocity = Vector3.zero; if (rb != null) { rb.isKinematic = false; rb.velocity = transform.forward * 20f; } // 조건이 맞으면 실행할거에요 -> 물리를 켜고 앞으로 속도 20으로 돌진을
rb.isKinematic = true; 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: 내려찍기
if (bossHealthBar != null) bossHealthBar.SetActive(false); 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) // 조건이 맞으면 실행할거에요 -> 쇠공과 물리 엔진이 존재한다면
{
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를
}
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("<color=red>[BossController] 보스 처치 처리 완료!</color>"); // 로그를 출력할거에요 -> 빨간 글씨로 처치 완료 메시지를
} }
} }

View File

@ -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 NorcielBoss boss; // 변수를 선언할거에요 -> 깨울 보스 스크립트(NorcielBoss)를 boss에
[SerializeField] private GameObject fogWall; // 보스 방 입구 막는 안개 (선택) [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. 보스 깨우기 // 1. 보스 깨우기
if (boss != null) boss.StartBossBattle(); if (boss != null) boss.StartBossBattle(); // 조건이 맞으면 실행할거에요 -> 보스가 연결되어 있다면 전투 시작 함수(StartBossBattle)를
// 2. 도망 못 가게 입구 막기 (선택) // 2. 도망 못 가게 입구 막기 (선택)
if (fogWall != null) fogWall.SetActive(true); if (fogWall != null) fogWall.SetActive(true); // 조건이 맞으면 실행할거에요 -> 안개벽이 연결되어 있다면 활성화(true)를
// 3. 이 트리거는 할 일 다 했으니 끄기 // 3. 이 트리거는 할 일 다 했으니 끄기
// gameObject.SetActive(false); // 바로 끄면 안개도 꺼질 수 있으니 주의 // gameObject.SetActive(false); // 바로 끄면 안개도 꺼질 수 있으니 주의
} }
} }
} }

View File

@ -1,10 +1,10 @@
/// <summary> /// <summary> // 요약 설명을 적을거에요 -> 아래 enum이 뭔지
/// 보스 카운터 패턴 타입 열거형 /// 보스 카운터 패턴 타입 열거형 // 설명할거에요 -> 보스가 어떤 카운터 패턴을 쓰는지 구분하는 타입
/// </summary> /// </summary> // 요약 설명을 끝낼거에요 -> 주석 블록을
public enum CounterType public enum CounterType // 열거형을 선언할거에요 -> CounterType이라는 이름의 enum을
{ { // 코드 블록을 시작할거에요 -> CounterType 범위를
None, None, // 값을 정의할거에요 -> 카운터 없음 상태를
Dodge, // 회피 남발 카운터 Dodge, // 값을 정의할거에요 -> 회피 남발 카운터 타입을
Aim, // 정조준 과다 카운터 Aim, // 값을 정의할거에요 -> 정조준 과다 카운터 타입을
Pierce // 관통 화살 편중 카운터 Pierce // 값을 정의할거에요 -> 관통 화살 편중 카운터 타입을
} } // 코드 블록을 끝낼거에요 -> CounterType을

View File

@ -1,4 +1,4 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
/// <summary> /// <summary>
/// 하이브리드 몬스터 스포너 (생성은 스포너 기준, 생존은 몬스터 기준) /// 하이브리드 몬스터 스포너 (생성은 스포너 기준, 생존은 몬스터 기준)
@ -6,52 +6,52 @@
/// - Life & Hibernate: 몬스터 기준 거리로 최적화 /// - Life & Hibernate: 몬스터 기준 거리로 최적화
/// - Wake Up: 범위 안으로 복귀 시 재활성화 /// - Wake Up: 범위 안으로 복귀 시 재활성화
/// </summary> /// </summary>
public class MonsterSpawner : MonoBehaviour public class MonsterSpawner : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterSpawner를
{ {
[Header("=== 생성 설정 (Birth - 스포너 기준) ===")] [Header("=== 생성 설정 (Birth - 스포너 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 생성 설정 (Birth - 스포너 기준) === 을
[Tooltip("스포너로부터 이 거리 안에 플레이어가 들어오면 몬스터 생성")] [Tooltip("스포너로부터 이 거리 안에 플레이어가 들어오면 몬스터 생성")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private float spawnRange = 25f; [SerializeField] private float spawnRange = 25f; // 변수를 선언할거에요 -> 몬스터 생성 거리(25.0)를 spawnRange에
[Tooltip("몬스터가 죽은 후 재생성까지 대기 시간")] [Tooltip("몬스터가 죽은 후 재생성까지 대기 시간")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private float respawnCooldown = 3f; [SerializeField] private float respawnCooldown = 3f; // 변수를 선언할거에요 -> 재소환 대기시간(3초)을 respawnCooldown에
[Tooltip("오브젝트 풀에서 가져올 몬스터 태그")] [Tooltip("오브젝트 풀에서 가져올 몬스터 태그")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private string mobTag = "NormalMob"; [SerializeField] private string mobTag = "NormalMob"; // 변수를 선언할거에요 -> 소환할 몬스터의 태그 이름("NormalMob")을 mobTag에
[Header("=== 최적화 설정 (Life & Hibernate - 몬스터 기준) ===")] [Header("=== 최적화 설정 (Life & Hibernate - 몬스터 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 최적화 설정 (Life & Hibernate - 몬스터 기준) === 을
[Tooltip("몬스터 본체로부터 이 거리를 벗어나면 강제 동면 (전투 중이어도!)")] [Tooltip("몬스터 본체로부터 이 거리를 벗어나면 강제 동면 (전투 중이어도!)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private float optimizationRange = 60f; [SerializeField] private float optimizationRange = 60f; // 변수를 선언할거에요 -> 몬스터 최적화(동면) 거리(60.0)를 optimizationRange에
// 내부 상태 // 내부 상태
private GameObject _myMonster; private GameObject _myMonster; // 변수를 선언할거에요 -> 소환된 몬스터 게임오브젝트를 담을 _myMonster를
private MonsterClass _monsterScript; private MonsterClass _monsterScript; // 변수를 선언할거에요 -> 몬스터의 스크립트(MonsterClass)를 담을 _monsterScript를
private Transform _player; private Transform _player; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 _player를
private float _nextSpawnTime; 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"); GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); // 오브젝트를 찾을거에요 -> "Player" 태그가 붙은 오브젝트를 playerObj에
if (playerObj != null) _player = playerObj.transform; 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 // 몬스터가 없거나 죽었으면 -> Birth
if (_myMonster == null || (_monsterScript != null && _monsterScript.IsDead)) if (_myMonster == null || (_monsterScript != null && _monsterScript.IsDead)) // 조건이 맞으면 실행할거에요 -> 몬스터가 없거나, 몬스터 스크립트가 있는데 죽은 상태라면
{ {
HandleBirth(); HandleBirth(); // 함수를 실행할거에요 -> 몬스터 생성 로직인 HandleBirth를
} }
// 몬스터가 살아있으면 -> Life & Hibernate // 몬스터가 살아있으면 -> Life & Hibernate
else else // 조건이 틀리면 실행할거에요 -> 몬스터가 살아서 활동 중이라면
{ {
HandleLifeAndHibernate(); HandleLifeAndHibernate(); // 함수를 실행할거에요 -> 생존 및 최적화(동면) 로직인 HandleLifeAndHibernate를
} }
} }
@ -59,25 +59,25 @@ public class MonsterSpawner : MonoBehaviour
// 🐣 BIRTH (생성) - 스포너 기준 // 🐣 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<MonsterClass>(); _monsterScript = _myMonster.GetComponent<MonsterClass>(); // 컴포넌트를 가져올거에요 -> 몬스터의 핵심 스크립트(MonsterClass)를 _monsterScript에
if (_monsterScript != null) _monsterScript.ResetStats(); if (_monsterScript != null) _monsterScript.ResetStats(); // 조건이 맞으면 실행할거에요 -> 스크립트가 있다면 몬스터의 상태(체력 등)를 초기화(ResetStats)
} }
} }
@ -85,43 +85,43 @@ public class MonsterSpawner : MonoBehaviour
// 💤 LIFE & HIBERNATE (생존/동면) - 몬스터 기준 // 💤 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 (동면) // 😴 HIBERNATE (동면)
HibernateMonster(); HibernateMonster(); // 함수를 실행할거에요 -> 몬스터를 잠재우는 HibernateMonster를
} }
else else // 조건이 틀리면 실행할거에요 -> 거리가 가깝다면
{ {
// 👁 WAKE UP (기상) // 👁 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 복구 (전투 중이었다면 추적 재개) // ⭐ AI 복구 (전투 중이었다면 추적 재개)
_monsterScript.Reactivate(); _monsterScript.Reactivate(); // 함수를 실행할거에요 -> 몬스터의 AI를 다시 작동시키는 Reactivate를
} }
} }
} }
@ -133,31 +133,31 @@ public class MonsterSpawner : MonoBehaviour
/// <summary> /// <summary>
/// 몬스터가 죽었을 때 호출 (쿨타임 적용) /// 몬스터가 죽었을 때 호출 (쿨타임 적용)
/// </summary> /// </summary>
public void NotifyMonsterDead() public void NotifyMonsterDead() // 함수를 선언할거에요 -> 외부에서 몬스터 사망을 알릴 때 쓰는 NotifyMonsterDead를
{ {
// 쿨타임 계산: 현재 시간 + 대기 시간 // 쿨타임 계산: 현재 시간 + 대기 시간
_nextSpawnTime = Time.time + respawnCooldown; _nextSpawnTime = Time.time + respawnCooldown; // 값을 저장할거에요 -> 현재 시간에 쿨타임을 더해서 다음 소환 시간(_nextSpawnTime)으로
// 몬스터 참조를 비워서 Update에서 다시 HandleBirth()로 넘어가게 함 // 몬스터 참조를 비워서 Update에서 다시 HandleBirth()로 넘어가게 함
_myMonster = null; _myMonster = null; // 값을 비울거에요 -> 소환된 몬스터 변수를 null로
_monsterScript = null; _monsterScript = null; // 값을 비울거에요 -> 몬스터 스크립트 변수를 null로
} }
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Gizmos // Gizmos
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 오브젝트 선택 시 기즈모를 그리는 OnDrawGizmosSelected를
{ {
// 스포너 생성 범위 (빨간색) // 스포너 생성 범위 (빨간색)
Gizmos.color = Color.red; Gizmos.color = Color.red; // 색상을 설정할거에요 -> 기즈모 색을 빨간색으로
Gizmos.DrawWireSphere(transform.position, spawnRange); Gizmos.DrawWireSphere(transform.position, spawnRange); // 그림을 그릴거에요 -> 스포너 위치에 생성 범위(spawnRange) 크기의 원을
// 몬스터 최적화 범위 (파란색) // 몬스터 최적화 범위 (파란색)
if (_myMonster != null) if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 소환된 몬스터가 있다면
{ {
Gizmos.color = Color.blue; Gizmos.color = Color.blue; // 색상을 설정할거에요 -> 기즈모 색을 파란색으로
Gizmos.DrawWireSphere(_myMonster.transform.position, optimizationRange); Gizmos.DrawWireSphere(_myMonster.transform.position, optimizationRange); // 그림을 그릴거에요 -> 몬스터 위치에 최적화 범위(optimizationRange) 크기의 원을
} }
} }
} }

View File

@ -1,24 +1,24 @@
using System.Collections.Generic; using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
public class MobUpdateManager : MonoBehaviour public class MobUpdateManager : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MobUpdateManager를
{ {
public static MobUpdateManager Instance; public static MobUpdateManager Instance; // 변수를 선언할거에요 -> 어디서든 접근 가능한 싱글톤 인스턴스 Instance를
// ⭐ Monster -> MonsterClass로 수정하여 에러 해결 // ⭐ Monster -> MonsterClass로 수정하여 에러 해결
private List<MonsterClass> _activeMobs = new List<MonsterClass>(); private List<MonsterClass> _activeMobs = new List<MonsterClass>(); // 리스트를 초기화할거에요 -> 활성화된 몬스터들을 담을 _activeMobs를
private void Awake() => Instance = this; private void Awake() => Instance = this; // 함수를 실행할거에요 -> 스크립트 시작 시 내 자신(this)을 Instance에 넣기를
// ⭐ 매개변수 타입도 MonsterClass로 변경 // ⭐ 매개변수 타입도 MonsterClass로 변경
public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob); public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에 추가하는 RegisterMob을
public void UnregisterMob(MonsterClass mob) => _activeMobs.Remove(mob); 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 호출 // 리스트를 돌며 카메라 최적화 로직이 담긴 OnManagedUpdate 호출
if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate(); if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate(); // 조건이 맞으면 실행할거에요 -> 몬스터가 존재한다면 관리 업데이트 함수(OnManagedUpdate)를
} }
} }
} }

View File

@ -1,35 +1,37 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
using System; // ⭐ Action 사용을 위해 필요 using System; // Action 이벤트를 쓰기 위해 불러올거에요 -> System을
public class TrainingDummy : MonoBehaviour, IDamageable public class TrainingDummy : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> 더미 오브젝트가 데미지를 받게 만들 TrainingDummy를
{ { // 코드 블록을 시작할거에요 -> TrainingDummy 범위를
[Header("더미 설정")]
[SerializeField] private float maxHP = 9999f;
private float _currentHP;
private Animator _animator; [Header("더미 설정")] // 인스펙터에 제목을 표시할거에요 -> 더미 설정을
[SerializeField] private float maxHP = 9999f; // 변수를 선언할거에요 -> 최대 체력을 9999로 설정하는 maxHP를
private float _currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHP를
// ⭐ UI가 이 신호를 듣고 게이지를 줄입니다. private Animator _animator; // 변수를 선언할거에요 -> 애니메이터 컴포넌트를 담을 _animator를
public event Action<float, float> OnHealthChanged;
private void Start() // ⭐ UI가 이 신호를 듣고 게이지를 줄입니다. // 설명을 적을거에요 -> UI 업데이트용 이벤트임을
{ public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> (현재체력, 최대체력) 알림을 보내는 OnHealthChanged를
_currentHP = maxHP;
_animator = GetComponent<Animator>();
}
public void TakeDamage(float amount) private void Start() // 함수를 선언할거에요 -> 시작할 때 1번 실행되는 Start를
{ { // 코드 블록을 시작할거에요 -> Start 범위를
_currentHP -= amount; _currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로 초기화
_animator = GetComponent<Animator>(); // 컴포넌트를 가져올거에요 -> Animator를 찾아 _animator에
} // 코드 블록을 끝낼거에요 -> Start를
// ⭐ UI에 현재 체력과 최대 체력 정보를 보냅니다. public void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 주면 호출되는 TakeDamage를
OnHealthChanged?.Invoke(_currentHP, maxHP); { // 코드 블록을 시작할거에요 -> TakeDamage 범위를
_currentHP -= amount; // 값을 뺄거에요 -> 현재 체력에서 데미지만큼 감소
Debug.Log($"<color=red>[더미]</color> 피격! 데미지: {amount} | 남은 체력: {_currentHP}"); // ⭐ UI에 현재 체력과 최대 체력 정보를 보냅니다. // 설명을 적을거에요 -> UI 갱신 트리거임을
OnHealthChanged?.Invoke(_currentHP, maxHP); // 이벤트를 호출할거에요 -> UI에게 체력 변화 전달
if (_animator != null) Debug.Log($"<color=red>[더미]</color> 피격! 데미지: {amount} | 남은 체력: {_currentHP}"); // 로그를 찍을거에요 -> 데미지/남은 체력 확인용
{
_animator.Play("Hit", 0, 0f); if (_animator != null) // 조건을 검사할거에요 -> 애니메이터가 있는지
} { // 코드 블록을 시작할거에요 -> 애니메이션 재생 처리
} _animator.Play("Hit", 0, 0f); // 애니메이션을 재생할거에요 -> Hit 상태를 0번 레이어에서 처음부터
} } // 코드 블록을 끝낼거에요 -> 애니메이션 처리
} // 코드 블록을 끝낼거에요 -> TakeDamage를
} // 코드 블록을 끝낼거에요 -> TrainingDummy를

View File

@ -1,18 +1,20 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
public class HealthPotion : MonoBehaviour public class HealthPotion : MonoBehaviour // 클래스를 선언할거에요 -> 체력 포션 아이템인 HealthPotion을
{ { // 코드 블록을 시작할거에요 -> HealthPotion 범위를
[Header("--- 포션 설정 ---")]
[SerializeField] private float healAmount = 30f; // 회복량
// PlayerInteraction에서 호출할 함수 [Header("--- 포션 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> --- 포션 설정 --- 을
public void Use(PlayerHealth target) [SerializeField] private float healAmount = 30f; // 변수를 선언할거에요 -> 회복량(30)을 healAmount에
{
if (target == null) return;
target.Heal(healAmount); // 플레이어 체력 회복 // PlayerInteraction에서 호출할 함수 // 설명을 적을거에요 -> 상호작용 스크립트가 이걸 실행한다는 걸
Debug.Log($"<color=green>[Potion]</color> 체력 {healAmount} 회복!"); public void Use(PlayerHealth target) // 함수를 선언할거에요 -> 대상 플레이어를 회복시키는 Use를
{ // 코드 블록을 시작할거에요 -> Use 범위를
if (target == null) return; // 조건이 맞으면 종료할거에요 -> 대상이 없으면 아무것도 안 함
Destroy(gameObject); // 사용 후 아이템 삭제 target.Heal(healAmount); // 함수를 실행할거에요 -> 플레이어 체력을 healAmount 만큼 회복
} Debug.Log($"<color=green>[Potion]</color> 체력 {healAmount} 회복!"); // 로그를 찍을거에요 -> 회복 로그 출력
}
Destroy(gameObject); // 오브젝트를 삭제할거에요 -> 사용한 포션 아이템 제거
} // 코드 블록을 끝낼거에요 -> Use를
} // 코드 블록을 끝낼거에요 -> HealthPotion을

View File

@ -1,63 +1,65 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
public class ItemHighlight : MonoBehaviour public class ItemHighlight : MonoBehaviour // 클래스를 선언할거에요 -> 아이템 하이라이트(발광 깜빡임) 담당 ItemHighlight를
{ { // 코드 블록을 시작할거에요 -> ItemHighlight 범위를
[Header("--- 참조 ---")]
[SerializeField] private MeshRenderer weaponRenderer;
[Header("--- 하이라이트 설정 ---")] [Header("--- 참조 ---")] // 인스펙터에 제목을 표시할거에요 -> 참조 섹션을
[ColorUsage(true, true)] [SerializeField] private MeshRenderer weaponRenderer; // 변수를 선언할거에요 -> 하이라이트 줄 렌더러를 weaponRenderer에
[SerializeField] private Color highlightColor = Color.yellow;
[SerializeField] private float blinkSpeed = 2f;
[SerializeField] private float minIntensity = 0.5f;
[SerializeField] private float maxIntensity = 3.0f;
private Material _material; [Header("--- 하이라이트 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> 하이라이트 설정 섹션을
private static readonly int EmissionProperty = Shader.PropertyToID("_EmissionColor"); [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() private Material _material; // 변수를 선언할거에요 -> 렌더러의 머티리얼을 담을 _material을
{ private static readonly int EmissionProperty = Shader.PropertyToID("_EmissionColor"); // 값을 캐싱할거에요 -> _EmissionColor 프로퍼티 ID를
if (weaponRenderer == null) weaponRenderer = GetComponentInChildren<MeshRenderer>();
if (weaponRenderer != null) private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를
{ { // 코드 블록을 시작할거에요 -> Awake 범위를
_material = weaponRenderer.material; if (weaponRenderer == null) weaponRenderer = GetComponentInChildren<MeshRenderer>(); // 조건이 맞으면 실행할거에요 -> 렌더러가 비어있으면 자식에서 자동으로 찾기
_material.EnableKeyword("_EMISSION");
}
}
// ⭐ [추가] 부모 오브젝트(손)가 바뀌었을 때 실행됩니다. if (weaponRenderer != null) // 조건을 검사할거에요 -> 렌더러를 찾았는지
private void OnTransformParentChanged() { // 코드 블록을 시작할거에요 -> 렌더러가 있을 때 처리
{ _material = weaponRenderer.material; // 머티리얼을 가져올거에요 -> 렌더러의 material을 _material에
// 부모가 없다면 = 바닥에 떨어졌다면 _material.EnableKeyword("_EMISSION"); // 키워드를 켤거에요 -> 발광(Emission) 기능을 활성화
if (transform.parent == null) } // 코드 블록을 끝낼거에요 -> 렌더러 처리
{ } // 코드 블록을 끝낼거에요 -> Awake를
this.enabled = true; // 스크립트를 다시 깨웁니다.
}
}
private void Update() // ⭐ [추가] 부모 오브젝트(손)가 바뀌었을 때 실행됩니다. // 설명을 적을거에요 -> 부모 변경 이벤트 훅임을
{ private void OnTransformParentChanged() // 함수를 선언할거에요 -> 부모가 바뀌면 자동 호출되는 OnTransformParentChanged를
if (_material == null) return; { // 코드 블록을 시작할거에요 -> OnTransformParentChanged 범위를
// 부모가 없다면 = 바닥에 떨어졌다면 // 설명을 적을거에요 -> 다시 주워지지 않은 상태라는 뜻
if (transform.parent == null) // 조건을 검사할거에요 -> 부모가 없는지
{ // 코드 블록을 시작할거에요 -> 바닥에 떨어진 경우 처리
this.enabled = true; // 스크립트를 켤거에요 -> Update가 다시 돌게
} // 코드 블록을 끝낼거에요 -> 바닥 처리
} // 코드 블록을 끝낼거에요 -> OnTransformParentChanged를
// 누군가 주운 상태(부모가 있음)라면 빛을 끄고 퇴근합니다. private void Update() // 함수를 선언할거에요 -> 매 프레임 실행되는 Update를
if (transform.parent != null) { // 코드 블록을 시작할거에요 -> Update 범위를
{ if (_material == null) return; // 조건이 맞으면 종료할거에요 -> 머티리얼이 없으면 처리 불가
StopHighlight();
return;
}
float lerpValue = Mathf.PingPong(Time.time * blinkSpeed, 1f); // 누군가 주운 상태(부모가 있음)라면 빛을 끄고 퇴근합니다. // 설명을 적을거에요 -> 손에 들린 상태면 하이라이트 중단
float intensity = Mathf.Lerp(minIntensity, maxIntensity, lerpValue); if (transform.parent != null) // 조건을 검사할거에요 -> 부모가 있는지(주워졌는지)
_material.SetColor(EmissionProperty, highlightColor * intensity); { // 코드 블록을 시작할거에요 -> 주워진 경우 처리
} StopHighlight(); // 함수를 실행할거에요 -> 하이라이트 끄기
return; // 함수를 끝낼거에요 -> 아래 발광 계산은 안 하게
} // 코드 블록을 끝낼거에요 -> 주워진 처리
public void StopHighlight() float lerpValue = Mathf.PingPong(Time.time * blinkSpeed, 1f); // 값을 계산할거에요 -> 0~1로 왕복하는 값(깜빡임용)을
{ float intensity = Mathf.Lerp(minIntensity, maxIntensity, lerpValue); // 값을 계산할거에요 -> 최소~최대 사이 발광 세기를
if (_material != null) _material.SetColor(EmissionProperty, highlightColor * intensity); // 값을 설정할거에요 -> 발광 색상을 (색 * 세기)로
{ } // 코드 블록을 끝낼거에요 -> Update를
_material.SetColor(EmissionProperty, Color.black);
this.enabled = false; // Update를 멈춥니다. public void StopHighlight() // 함수를 선언할거에요 -> 하이라이트를 끄는 StopHighlight를
} { // 코드 블록을 시작할거에요 -> StopHighlight 범위를
} if (_material != null) // 조건을 검사할거에요 -> 머티리얼이 있는지
} { // 코드 블록을 시작할거에요 -> 머티리얼이 있을 때 처리
_material.SetColor(EmissionProperty, Color.black); // 값을 설정할거에요 -> 발광을 검정(=0)으로 만들어 끄기
this.enabled = false; // 컴포넌트를 끌거에요 -> Update를 멈추기
} // 코드 블록을 끝낼거에요 -> 머티리얼 처리
} // 코드 블록을 끝낼거에요 -> StopHighlight를
} // 코드 블록을 끝낼거에요 -> ItemHighlight를

View File

@ -1,123 +1,125 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
public class BossPatternPhases : MonoBehaviour public class BossPatternPhases : MonoBehaviour // 클래스를 선언할거에요 -> 보스 패턴 페이즈 진행을 관리하는 BossPatternPhases를
{ { // 코드 블록을 시작할거에요 -> BossPatternPhases 범위를
// [Header]는 유니티 Inspector 창에서 폴더처럼 보이게 해줌
[Header("🔧 패턴 설정")]
[SerializeField] private int totalPhases = 3; // 전체 패턴 구간 수 (Inspector에서 조절 가능)
[Header("💎 XP 설정")] // [Header]는 유니티 Inspector 창에서 폴더처럼 보이게 해줌 // 설명을 적을거에요 -> 인스펙터 그룹 라벨용
[SerializeField] private int phaseClearXP = 10; // 한 구간 통과 시 주는 XP [Header("🔧 패턴 설정")] // 인스펙터에 제목을 표시할거에요 -> 패턴 설정을
[SerializeField] private int fullClearBonus = 30; // 모든 구간 통과 시 보너스 XP [SerializeField] private int totalPhases = 3; // 변수를 선언할거에요 -> 전체 페이즈 수를 3으로
[SerializeField] private int hitPenalty = -5; // 피격 시 감점 XP (0으로 하면 감점 없음)
// [SerializeField]는 인스펙터에서 실시간으로 볼 수 있게 해줌 [Header("💎 XP 설정")] // 인스펙터에 제목을 표시할거에요 -> XP 설정을
[Header("🚩 상태 확인용")] [SerializeField] private int phaseClearXP = 10; // 변수를 선언할거에요 -> 페이즈 성공 시 XP를 10으로
[SerializeField] private int currentPhase = 0; // 현재 진행 중인 구간 (0: 시작 전) [SerializeField] private int fullClearBonus = 30; // 변수를 선언할거에요 -> 전체 클리어 보너스를 30으로
[SerializeField] private bool isPatternActive = false; // 패턴이 진행 중인지 [SerializeField] private int hitPenalty = -5; // 변수를 선언할거에요 -> 피격 시 감점 XP를 -5로
[SerializeField] private bool wasHitThisPhase = false; // 이번 구간 중 피격당했는지
// ★ 초기화 // [SerializeField]는 인스펙터에서 실시간으로 볼 수 있게 해줌 // 설명을 적을거에요 -> 런타임에도 값 확인 가능
private void Start() [Header("🚩 상태 확인용")] // 인스펙터에 제목을 표시할거에요 -> 상태 확인용을
{ [SerializeField] private int currentPhase = 0; // 변수를 선언할거에요 -> 현재 페이즈를 0으로(시작 전)
ResetPattern(); [SerializeField] private bool isPatternActive = false; // 변수를 선언할거에요 -> 패턴 진행 여부를 false로
} [SerializeField] private bool wasHitThisPhase = false; // 변수를 선언할거에요 -> 이번 페이즈 피격 여부를 false로
// ★ 패턴 전체 리셋 (새로운 패턴 시작 시 호출) // ★ 초기화 // 설명을 적을거에요 -> 시작 시 초기화 수행
public void ResetPattern() private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를
{ { // 코드 블록을 시작할거에요 -> Start 범위를
currentPhase = 0; ResetPattern(); // 함수를 실행할거에요 -> 패턴 상태를 초기화
wasHitThisPhase = false; } // 코드 블록을 끝낼거에요 -> Start를
isPatternActive = false;
Debug.Log("[보스] 패턴 초기화 완료!");
}
// ★ 패턴 시작 (보스 AI에서 호출) // ★ 패턴 전체 리셋 (새로운 패턴 시작 시 호출) // 설명을 적을거에요 -> 상태를 처음으로 되돌리는 함수
public void StartPattern() public void ResetPattern() // 함수를 선언할거에요 -> 패턴 전체 초기화 ResetPattern을
{ { // 코드 블록을 시작할거에요 -> ResetPattern 범위를
if (currentPhase >= totalPhases) currentPhase = 0; // 값을 설정할거에요 -> 현재 페이즈를 0으로
{ wasHitThisPhase = false; // 값을 설정할거에요 -> 피격 여부를 false로
Debug.Log("이미 모든 패턴을 완료했어요!"); isPatternActive = false; // 값을 설정할거에요 -> 패턴 진행 상태를 false로
return; Debug.Log("[보스] 패턴 초기화 완료!"); // 로그를 찍을거에요 -> 초기화 완료 확인
} } // 코드 블록을 끝낼거에요 -> ResetPattern을
isPatternActive = true; // ★ 패턴 시작 (보스 AI에서 호출) // 설명을 적을거에요 -> 보스가 패턴을 시작할 때 호출
wasHitThisPhase = false; public void StartPattern() // 함수를 선언할거에요 -> 패턴 시작 함수 StartPattern을
Debug.Log($"🔔 [페이즈 {currentPhase + 1}] 시작! 회피하세요!"); { // 코드 블록을 시작할거에요 -> StartPattern 범위를
if (currentPhase >= totalPhases) // 조건을 검사할거에요 -> 이미 전부 끝났는지
{ // 코드 블록을 시작할거에요 -> 전부 끝난 경우 처리
Debug.Log("이미 모든 패턴을 완료했어요!"); // 로그를 찍을거에요 -> 더 이상 시작 불가 안내
return; // 함수를 끝낼거에요 -> 아래 로직 실행 안 하게
} // 코드 블록을 끝낼거에요 -> 완료 처리
// ★ ★ ★ 여기서 보스의 애니메이션/이펙트 시작하면 됨 ★ ★ ★ isPatternActive = true; // 값을 설정할거에요 -> 패턴 진행 상태를 true로
} wasHitThisPhase = false; // 값을 설정할거에요 -> 이번 페이즈 피격 여부 초기화
Debug.Log($"🔔 [페이즈 {currentPhase + 1}] 시작! 회피하세요!"); // 로그를 찍을거에요 -> 페이즈 시작 알림
// ★ 플레이어가 피격당했을 때 호출 (PlayerHealth에서 연결) // ★ ★ ★ 여기서 보스의 애니메이션/이펙트 시작하면 됨 ★ ★ ★ // 설명을 적을거에요 -> 연출 시작 지점
public void OnPlayerHit() } // 코드 블록을 끝낼거에요 -> StartPattern을
{
if (!isPatternActive) return;
wasHitThisPhase = true; // ★ 플레이어가 피격당했을 때 호출 (PlayerHealth에서 연결) // 설명을 적을거에요 -> 플레이어 피격 이벤트 연결용
Debug.Log($"💥 [페이즈 {currentPhase + 1}] 중 피격당함! (XP 감점)"); public void OnPlayerHit() // 함수를 선언할거에요 -> 피격 처리 함수 OnPlayerHit을
{ // 코드 블록을 시작할거에요 -> OnPlayerHit 범위를
if (!isPatternActive) return; // 조건이 맞으면 종료할거에요 -> 패턴 중이 아니면 무시
// 피격 시 감점 처리 (필요하면) wasHitThisPhase = true; // 값을 설정할거에요 -> 이번 페이즈에서 맞았다고 표시
if (hitPenalty != 0 && ObsessionSystem.instance != null) Debug.Log($"💥 [페이즈 {currentPhase + 1}] 중 피격당함! (XP 감점)"); // 로그를 찍을거에요 -> 피격 안내
{
ObsessionSystem.instance.AddRunXP(hitPenalty);
}
}
// ★ 현재 구간을 성공적으로 통과했을 때 호출 (보스 AI에서 호출) // 피격 시 감점 처리 (필요하면) // 설명을 적을거에요 -> hitPenalty가 0이 아니면 XP 감소
public void ClearCurrentPhase() if (hitPenalty != 0 && ObsessionSystem.instance != null) // 조건을 검사할거에요 -> 감점이 있고 시스템이 존재하는지
{ { // 코드 블록을 시작할거에요 -> 감점 처리
if (!isPatternActive) return; ObsessionSystem.instance.AddRunXP(hitPenalty); // XP를 더할거에요 -> 음수면 감점됨
} // 코드 블록을 끝낼거에요 -> 감점 처리
} // 코드 블록을 끝낼거에요 -> OnPlayerHit을
// 피격당했는지 확인 // ★ 현재 구간을 성공적으로 통과했을 때 호출 (보스 AI에서 호출) // 설명을 적을거에요 -> 페이즈 종료/판정 처리
if (wasHitThisPhase) public void ClearCurrentPhase() // 함수를 선언할거에요 -> 현재 페이즈 클리어 처리 ClearCurrentPhase를
{ { // 코드 블록을 시작할거에요 -> ClearCurrentPhase 범위를
Debug.Log($"❌ [페이즈 {currentPhase + 1}] 실패... 다음 구간 진행"); if (!isPatternActive) return; // 조건이 맞으면 종료할거에요 -> 패턴 중이 아니면 무시
}
else
{
// ★ 성공 시 XP 지급
if (ObsessionSystem.instance != null)
{
ObsessionSystem.instance.AddRunXP(phaseClearXP);
Debug.Log($"✅ [페이즈 {currentPhase + 1}] 성공! +{phaseClearXP}XP");
}
}
// 다음 구간으로 // 피격당했는지 확인 // 설명을 적을거에요 -> 성공/실패 판정 기준
currentPhase++; if (wasHitThisPhase) // 조건을 검사할거에요 -> 이번 페이즈 중 피격 여부
wasHitThisPhase = false; { // 코드 블록을 시작할거에요 -> 실패 처리
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) currentPhase++; // 값을 증가시킬거에요 -> 다음 페이즈로 이동
{ wasHitThisPhase = false; // 값을 초기화할거에요 -> 다음 페이즈를 위해 피격 여부 리셋
Debug.Log("🎉 모든 패턴 완료!");
FullClearBonus();
isPatternActive = false;
}
}
// ★ 모든 구간 통과 시 보너스 // 모든 구간을 완료했는가? // 설명을 적을거에요 -> 전체 클리어 체크
private void FullClearBonus() if (currentPhase >= totalPhases) // 조건을 검사할거에요 -> 마지막 페이즈까지 끝났는지
{ { // 코드 블록을 시작할거에요 -> 전체 클리어 처리
if (ObsessionSystem.instance != null) Debug.Log("🎉 모든 패턴 완료!"); // 로그를 찍을거에요 -> 전체 완료 안내
{ FullClearBonus(); // 함수를 실행할거에요 -> 보너스 XP 지급
ObsessionSystem.instance.AddRunXP(fullClearBonus); isPatternActive = false; // 값을 설정할거에요 -> 패턴 종료 상태로
Debug.Log($"🌟 전체 패턴 클리어 보너스 +{fullClearBonus}XP!"); } // 코드 블록을 끝낼거에요 -> 전체 클리어 처리
} } // 코드 블록을 끝낼거에요 -> ClearCurrentPhase를
}
// ★ 디버그용: 현재 상태를 콘솔에 출력 // ★ 모든 구간 통과 시 보너스 // 설명을 적을거에요 -> 전체 클리어 보너스 지급
private void Update() private void FullClearBonus() // 함수를 선언할거에요 -> 전체 클리어 보너스 FullClearBonus를
{ { // 코드 블록을 시작할거에요 -> FullClearBonus 범위를
if (Input.GetKeyDown(KeyCode.P)) if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> XP 시스템이 존재하는지
{ { // 코드 블록을 시작할거에요 -> 보너스 지급
Debug.Log($"[패턴 상태] 현재 구간: {currentPhase + 1}/{totalPhases}, 활성: {isPatternActive}"); ObsessionSystem.instance.AddRunXP(fullClearBonus); // XP를 더할거에요 -> 보너스 XP 지급
} Debug.Log($"🌟 전체 패턴 클리어 보너스 +{fullClearBonus}XP!"); // 로그를 찍을거에요 -> 보너스 안내
} } // 코드 블록을 끝낼거에요 -> 보너스 지급
} // 코드 블록을 끝낼거에요 -> FullClearBonus를
// BossPatternPhases.cs에 추가 // ★ 디버그용: 현재 상태를 콘솔에 출력 // 설명을 적을거에요 -> 테스트용 단축키 출력
public bool IsPatternActive() private void Update() // 함수를 선언할거에요 -> 매 프레임 호출되는 Update를
{ { // 코드 블록을 시작할거에요 -> Update 범위를
return isPatternActive; if (Input.GetKeyDown(KeyCode.P)) // 조건을 검사할거에요 -> P 키를 눌렀는지
} { // 코드 블록을 시작할거에요 -> 디버그 출력
} Debug.Log($"[패턴 상태] 현재 구간: {currentPhase + 1}/{totalPhases}, 활성: {isPatternActive}"); // 로그를 찍을거에요 -> 현재 상태를
} // 코드 블록을 끝낼거에요 -> 디버그 출력
} // 코드 블록을 끝낼거에요 -> Update를
// BossPatternPhases.cs에 추가 // 설명을 적을거에요 -> 외부에서 패턴 진행 여부 확인용
public bool IsPatternActive() // 함수를 선언할거에요 -> 패턴 활성 여부 반환 함수 IsPatternActive를
{ // 코드 블록을 시작할거에요 -> IsPatternActive 범위를
return isPatternActive; // 값을 돌려줄거에요 -> 현재 활성 상태를
} // 코드 블록을 끝낼거에요 -> IsPatternActive를
} // 코드 블록을 끝낼거에요 -> BossPatternPhases를

View File

@ -1,19 +1,22 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
public class BossRoomTrigger : MonoBehaviour public class BossRoomTrigger : MonoBehaviour // 클래스를 선언할거에요 -> 보스방 진입 트리거를 처리하는 BossRoomTrigger를
{ { // 코드 블록을 시작할거에요 -> BossRoomTrigger 범위를
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
// ★ 보스 방에 처음 들어가면 보너스 XP
if (ObsessionSystem.instance != null)
{
ObsessionSystem.instance.AddRunXP(50); // 보스 도달 보너스
Debug.Log("보스 방 도달! +50XP 보너스 획득!");
}
Destroy(this); // 한 번만 주고 제거 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를

View File

@ -1,118 +1,110 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
public class ObsessionSystem : MonoBehaviour public class ObsessionSystem : MonoBehaviour // 클래스를 선언할거에요 -> 영구 XP/런 XP/레벨을 관리하는 ObsessionSystem을
{ { // 코드 블록을 시작할거에요 -> ObsessionSystem 범위를
public static ObsessionSystem instance;
[SerializeField] private int currentXP = 0; // ★ 영구 XP (저장되는 진짜 진행도) public static ObsessionSystem instance; // 정적 변수를 선언할거에요 -> 어디서든 접근 가능한 싱글톤 instance를
[SerializeField] private int currentLevel = 1;
[SerializeField] private int[] xpRequirements = { 100, 300, 500, 800, 1200 };
// ★ ★ ★ NEW: 이번 런에서 쌓은 임시 XP ★ ★ ★ [SerializeField] private int currentXP = 0; // 변수를 선언할거에요 -> 영구 XP(저장되는 진행도)를 currentXP에
[SerializeField] private int currentRunXP = 0; // 이 값은 게임 껐다 키면 사라져요! [SerializeField] private int currentLevel = 1; // 변수를 선언할거에요 -> 현재 레벨을 currentLevel에
[SerializeField] private int[] xpRequirements = { 100, 300, 500, 800, 1200 }; // 배열을 선언할거에요 -> 레벨업 필요 XP 목록을
private void Awake() [SerializeField] private int currentRunXP = 0; // 변수를 선언할거에요 -> 이번 런에서 쌓인 임시 XP를 currentRunXP에
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
private void Start() private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를
{ { // 코드 블록을 시작할거에요 -> Awake 범위를
LoadData(); if (instance == null) // 조건을 검사할거에요 -> 싱글톤이 아직 없으면
Debug.Log($"[집착 시스템] 시작! 현재 레벨: {currentLevel}, XP: {currentXP}"); { // 코드 블록을 시작할거에요 -> 최초 생성 처리
instance = this; // 값을 넣을거에요 -> instance에 자신을 등록
DontDestroyOnLoad(gameObject); // 파괴하지 않을거에요 -> 씬이 바뀌어도 유지되게
} // 코드 블록을 끝낼거에요 -> 최초 생성 처리
else // 조건이 아니면 실행할거에요 -> 이미 instance가 있으면
{ // 코드 블록을 시작할거에요 -> 중복 제거 처리
Destroy(gameObject); // 파괴할거에요 -> 중복 오브젝트를
} // 코드 블록을 끝낼거에요 -> 중복 제거 처리
} // 코드 블록을 끝낼거에요 -> Awake를
// ★ ★ ★ NEW: 새 런 시작할 때 임시 XP는 0으로 초기화 ★ ★ ★ private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를
currentRunXP = 0; { // 코드 블록을 시작할거에요 -> Start 범위를
Debug.Log($"[새 런 시작] 이번 런에 쌓을 임시 XP: 0"); LoadData(); // 함수를 실행할거에요 -> 저장된 데이터 불러오기
} Debug.Log($"[집착 시스템] 시작! 현재 레벨: {currentLevel}, XP: {currentXP}"); // 로그를 찍을거에요 -> 시작 상태 출력
// ★ ★ ★ NEW: 플레이 중 실시간으로 임시 XP 추가 ★ ★ ★ currentRunXP = 0; // 값을 초기화할거에요 -> 새 런 시작이니 임시 XP를 0으로
public void AddRunXP(int amount) Debug.Log($"[새 런 시작] 이번 런에 쌓을 임시 XP: 0"); // 로그를 찍을거에요 -> 런 XP 초기화 확인
{ } // 코드 블록을 끝낼거에요 -> Start를
currentRunXP += amount;
Debug.Log($"[런 XP 획득] +{amount}XP (이번 런 총: {currentRunXP})");
}
// ★ ★ ★ NEW: 사망 시 임시 XP를 영구 XP로 변환 ★ ★ ★ public void AddRunXP(int amount) // 함수를 선언할거에요 -> 플레이 중 임시 XP를 더하는 AddRunXP를
public void OnDeathConvertXP() { // 코드 블록을 시작할거에요 -> AddRunXP 범위를
{ currentRunXP += amount; // 값을 더할거에요 -> 임시 XP에 amount만큼
Debug.Log($"[사망] 이번 런 성과: {currentRunXP}XP를 영구 XP로 전환!"); Debug.Log($"[런 XP 획득] +{amount}XP (이번 런 총: {currentRunXP})"); // 로그를 찍을거에요 -> 현재 런 XP 출력
} // 코드 블록을 끝낼거에요 -> AddRunXP를
// 임시 XP를 영구 XP에 추가 public void OnDeathConvertXP() // 함수를 선언할거에요 -> 사망 시 임시 XP를 영구 XP로 바꾸는 OnDeathConvertXP를
AddXP(currentRunXP); { // 코드 블록을 시작할거에요 -> OnDeathConvertXP 범위를
Debug.Log($"[사망] 이번 런 성과: {currentRunXP}XP를 영구 XP로 전환!"); // 로그를 찍을거에요 -> 전환 안내
// 임시 XP는 초기화 (다음 런을 위해) AddXP(currentRunXP); // 함수를 실행할거에요 -> 임시 XP만큼 영구 XP에 추가(레벨업/저장 포함)
currentRunXP = 0;
}
// 기존 AddXP 함수 (이제는 사망할 때만 호출될 거예요) currentRunXP = 0; // 값을 초기화할거에요 -> 다음 런을 위해 임시 XP를 0으로
public void AddXP(int amount) } // 코드 블록을 끝낼거에요 -> OnDeathConvertXP를
{
currentXP += amount;
Debug.Log($"[영구 XP 획득] +{amount}XP (현재: {currentXP})");
CheckLevelUp();
SaveData();
}
private void CheckLevelUp() public void AddXP(int amount) // 함수를 선언할거에요 -> 영구 XP를 더하는 AddXP를
{ { // 코드 블록을 시작할거에요 -> AddXP 범위를
if (currentLevel - 1 < xpRequirements.Length) currentXP += amount; // 값을 더할거에요 -> 영구 XP에 amount만큼
{ Debug.Log($"[영구 XP 획득] +{amount}XP (현재: {currentXP})"); // 로그를 찍을거에요 -> 영구 XP 현황 출력
int requiredXP = xpRequirements[currentLevel - 1]; CheckLevelUp(); // 함수를 실행할거에요 -> 레벨업 조건 확인
if (currentXP >= requiredXP) SaveData(); // 함수를 실행할거에요 -> 변경된 데이터 저장
{ } // 코드 블록을 끝낼거에요 -> AddXP를
currentLevel++;
Debug.Log($"🎉 [레벨업!] 레벨 {currentLevel - 1} → {currentLevel}");
UnlockRewards(currentLevel);
}
}
}
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() private void UnlockRewards(int level) // 함수를 선언할거에요 -> 레벨 보상을 해금하는 UnlockRewards를
{ { // 코드 블록을 시작할거에요 -> UnlockRewards 범위를
PlayerPrefs.SetInt("ObsessionLevel", currentLevel); // 보상 코드는 기존과 동일 // 설명을 적을거에요 -> 보상 구현은 여기에 추가하면 됨
PlayerPrefs.SetInt("ObsessionXP", currentXP); } // 코드 블록을 끝낼거에요 -> UnlockRewards를
PlayerPrefs.Save();
}
private void LoadData() private void SaveData() // 함수를 선언할거에요 -> PlayerPrefs에 저장하는 SaveData를
{ { // 코드 블록을 시작할거에요 -> SaveData 범위를
currentLevel = PlayerPrefs.GetInt("ObsessionLevel", 1); PlayerPrefs.SetInt("ObsessionLevel", currentLevel); // 값을 저장할거에요 -> 레벨을 ObsessionLevel 키로
currentXP = PlayerPrefs.GetInt("ObsessionXP", 0); 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() // 🧪 테스트용: 현재 상태를 읽는 함수들 // 설명을 적을거에요 -> 외부 조회용 getter들
{ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 구분선을 적을거에요 -> 테스트/조회 섹션을
return currentRunXP;
}
// 현재 영구 XP 확인 public int GetCurrentRunXP() // 함수를 선언할거에요 -> 현재 런 XP를 반환하는 GetCurrentRunXP를
public int GetCurrentXP() { // 코드 블록을 시작할거에요 -> GetCurrentRunXP 범위를
{ return currentRunXP; // 값을 돌려줄거에요 -> 임시 XP를
return currentXP; } // 코드 블록을 끝낼거에요 -> GetCurrentRunXP를
}
// 현재 레벨 확인 public int GetCurrentXP() // 함수를 선언할거에요 -> 현재 영구 XP를 반환하는 GetCurrentXP를
public int GetCurrentLevel() { // 코드 블록을 시작할거에요 -> GetCurrentXP 범위를
{ return currentXP; // 값을 돌려줄거에요 -> 영구 XP를
return currentLevel; } // 코드 블록을 끝낼거에요 -> GetCurrentXP를
}
} public int GetCurrentLevel() // 함수를 선언할거에요 -> 현재 레벨을 반환하는 GetCurrentLevel을
{ // 코드 블록을 시작할거에요 -> GetCurrentLevel 범위를
return currentLevel; // 값을 돌려줄거에요 -> 현재 레벨을
} // 코드 블록을 끝낼거에요 -> GetCurrentLevel을
} // 코드 블록을 끝낼거에요 -> ObsessionSystem을

View File

@ -1,14 +1,18 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
public class RoomClear : MonoBehaviour public class RoomClear : MonoBehaviour // 클래스를 선언할거에요 -> 방 클리어 보상을 주는 RoomClear를
{ { // 코드 블록을 시작할거에요 -> RoomClear 범위를
public void ClearRoom()
{ public void ClearRoom() // 함수를 선언할거에요 -> 방을 클리어했을 때 호출할 ClearRoom을
// ★ 방을 무사히 클리어하면 { // 코드 블록을 시작할거에요 -> ClearRoom 범위를
if (ObsessionSystem.instance != null)
{ // ★ 방을 무사히 클리어하면 // 설명을 적을거에요 -> 클리어 보상 지급 구간
ObsessionSystem.instance.AddRunXP(20); if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> 집착 시스템이 존재하는지
Debug.Log("방 클리어! +20XP"); { // 코드 블록을 시작할거에요 -> XP 지급 처리
} ObsessionSystem.instance.AddRunXP(20); // 함수를 실행할거에요 -> 이번 런 XP에 20 추가
} Debug.Log("방 클리어! +20XP"); // 로그를 찍을거에요 -> 보상 지급 확인
} } // 코드 블록을 끝낼거에요 -> XP 지급 처리
} // 코드 블록을 끝낼거에요 -> ClearRoom을
} // 코드 블록을 끝낼거에요 -> RoomClear를

View File

@ -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 Animator anim; // 변수를 선언할거에요 -> 애니메이터 컴포넌트 anim을
[SerializeField] private PlayerHealth health; [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.OnHitEvent += () => anim.SetTrigger("Hit"); // 이벤트를 구독할거에요 -> 피격 시 Hit 트리거 발동을
health.OnDead += () => anim.SetBool("Die", true); 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 UpdateMove(float speedValue) => anim.SetFloat("Speed", speedValue); // 값을 전달할거에요 -> 애니메이터의 Speed 파라미터에
// public void TriggerAttack() => anim.SetTrigger("Attack"); // public void TriggerAttack() => anim.SetTrigger("Attack");
public void TriggerThrow() => anim.SetTrigger("Throw"); public void TriggerThrow() => anim.SetTrigger("Throw"); // 신호를 보낼거에요 -> 애니메이터의 Throw 트리거에
public void SetCharging(bool isCharging) => anim.SetBool("isCharging", isCharging); public void SetCharging(bool isCharging) => anim.SetBool("isCharging", isCharging); // 값을 설정할거에요 -> 애니메이터의 isCharging 불리언에
} }

View File

@ -1,57 +1,57 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
/// <summary> /// <summary>
/// 발사된 화살 발사체 (파티클 프리팹에 부착됨) /// 발사된 화살 발사체 (파티클 프리팹에 부착됨)
/// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지) /// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지)
/// [NEW] 속성 데미지(DoT) 시스템 추가 /// [NEW] 속성 데미지(DoT) 시스템 추가
/// </summary> /// </summary>
public class PlayerArrow : MonoBehaviour public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerArrow를
{ {
[Header("--- 정밀 피격 판정 설정 ---")] [Header("--- 정밀 피격 판정 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 정밀 피격 판정 설정 --- 을
[SerializeField] private float raycastDistance = 0.8f; [SerializeField] private float raycastDistance = 0.8f; // 변수를 선언할거에요 -> 레이캐스트 거리인 raycastDistance를
[SerializeField] private float raycastRadius = 0.08f; [SerializeField] private float raycastRadius = 0.08f; // 변수를 선언할거에요 -> 레이캐스트 반지름인 raycastRadius를
[SerializeField] private LayerMask hitLayers; [SerializeField] private LayerMask hitLayers; // 변수를 선언할거에요 -> 충돌 레이어인 hitLayers를
[SerializeField] private Transform arrowTip; [SerializeField] private Transform arrowTip; // 변수를 선언할거에요 -> 화살 끝부분 위치인 arrowTip을
[Header("--- 디버그 시각화 ---")] [Header("--- 디버그 시각화 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 디버그 시각화 --- 를
[SerializeField] private bool showDebugRay = true; [SerializeField] private bool showDebugRay = true; // 변수를 선언할거에요 -> 디버그 레이 표시 여부인 showDebugRay를
// 화살 스탯 // 화살 스탯
private float damage; private float damage; // 변수를 선언할거에요 -> 데미지 damage를
private float speed; private float speed; // 변수를 선언할거에요 -> 속도 speed를
private float range; private float range; // 변수를 선언할거에요 -> 사거리 range를
private Vector3 startPos; private Vector3 startPos; // 변수를 선언할거에요 -> 시작 위치 startPos를
private Vector3 shootDirection; private Vector3 shootDirection; // 변수를 선언할거에요 -> 발사 방향 shootDirection을
private bool isFired = false; private bool isFired = false; // 변수를 초기화할거에요 -> 발사 여부 isFired를 거짓으로
private bool hasHit = false; private bool hasHit = false; // 변수를 초기화할거에요 -> 충돌 여부 hasHit을 거짓으로
// [NEW] 속성 데미지 시스템 // [NEW] 속성 데미지 시스템
private ArrowElementType elementType = ArrowElementType.None; private ArrowElementType elementType = ArrowElementType.None; // 변수를 초기화할거에요 -> 속성 타입 elementType을 없음으로
private float elementDamage = 0f; private float elementDamage = 0f; // 변수를 초기화할거에요 -> 속성 데미지 elementDamage를 0으로
private float elementDuration = 0f; private float elementDuration = 0f; // 변수를 초기화할거에요 -> 속성 지속 시간 elementDuration을 0으로
// Raycast 추적용 // Raycast 추적용
private Vector3 previousPosition; private Vector3 previousPosition; // 변수를 선언할거에요 -> 이전 위치 previousPosition을
private Rigidbody rb; private Rigidbody rb; // 변수를 선언할거에요 -> 물리 컴포넌트 rb를
private void Awake() private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
{ {
rb = GetComponent<Rigidbody>(); rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 리지드바디를
} }
private void Start() private void Start() // 함수를 실행할거에요 -> 활성화 시 Start를
{ {
if (arrowTip == null) if (arrowTip == null) // 조건이 맞으면 실행할거에요 -> 화살 팁이 설정되지 않았다면
{ {
Transform tip = transform.Find("ArrowTip"); Transform tip = transform.Find("ArrowTip"); // 찾을거에요 -> 자식 중 ArrowTip을
if (tip == null) tip = transform.Find("Tip"); if (tip == null) tip = transform.Find("Tip"); // 못 찾으면 찾을거에요 -> Tip을
arrowTip = tip ?? transform; 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, Vector3 direction,
ArrowElementType element, ArrowElementType element,
float elemDmg, float elemDmg,
float elemDur) float elemDur) // 함수를 선언할거에요 -> 화살을 초기화하는 Initialize를
{ {
this.damage = dmg; this.damage = dmg; // 값을 저장할거에요 -> 데미지를
this.speed = arrowSpeed; this.speed = arrowSpeed; // 값을 저장할거에요 -> 속도를
this.range = maxRange; this.range = maxRange; // 값을 저장할거에요 -> 사거리를
this.shootDirection = direction.normalized; this.shootDirection = direction.normalized; // 값을 저장할거에요 -> 정규화된 발사 방향을
this.startPos = transform.position; this.startPos = transform.position; // 값을 저장할거에요 -> 시작 위치를
this.previousPosition = transform.position; this.previousPosition = transform.position; // 값을 저장할거에요 -> 이전 위치를 (레이캐스트용)
this.isFired = true; this.isFired = true; // 상태를 바꿀거에요 -> 발사됨 상태로
// [NEW] 속성 정보 저장 // [NEW] 속성 정보 저장
this.elementType = element; this.elementType = element; // 값을 저장할거에요 -> 속성 타입을
this.elementDamage = elemDmg; this.elementDamage = elemDmg; // 값을 저장할거에요 -> 속성 데미지를
this.elementDuration = elemDur; this.elementDuration = elemDur; // 값을 저장할거에요 -> 속성 지속 시간을
// 발사 방향으로 회전 // 발사 방향으로 회전
if (shootDirection != Vector3.zero) if (shootDirection != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 유효하다면
{ {
transform.rotation = Quaternion.LookRotation(shootDirection); transform.rotation = Quaternion.LookRotation(shootDirection); // 회전시킬거에요 -> 발사 방향을 보도록
} }
// Rigidbody 설정 // Rigidbody 설정
if (rb == null) rb = GetComponent<Rigidbody>(); if (rb == null) rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 없다면 리지드바디를
if (rb != null) if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
{ {
rb.useGravity = false; rb.useGravity = false; // 설정을 바꿀거에요 -> 중력을 끄기로
rb.isKinematic = false; rb.isKinematic = false; // 설정을 바꿀거에요 -> 물리 연산을 켜기로
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 설정을 바꿀거에요 -> 연속 충돌 감지로
rb.velocity = shootDirection * speed; rb.velocity = shootDirection * speed; // 값을 넣을거에요 -> 방향과 속도를 곱해 물리 속도로
} }
Destroy(gameObject, 5f); Destroy(gameObject, 5f); // 파괴할거에요 -> 5초 뒤에 안전장치로
} }
/// <summary> /// <summary>
/// [하위 호환성] 방향 없는 3-파라미터 버전 /// [하위 호환성] 방향 없는 3-파라미터 버전
/// </summary> /// </summary>
public void Initialize(float dmg, float arrowSpeed, float maxRange) public void Initialize(float dmg, float arrowSpeed, float maxRange) // 함수를 선언할거에요 -> 구버전 호환용 초기화 함수를
{ {
Initialize(dmg, arrowSpeed, maxRange, transform.forward, 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); float traveledDistance = Vector3.Distance(startPos, transform.position); // 거리를 계산할거에요 -> 이동 거리를
if (traveledDistance >= range) if (traveledDistance >= range) // 조건이 맞으면 실행할거에요 -> 사거리를 넘었다면
{ {
Destroy(gameObject); Destroy(gameObject); // 파괴할거에요 -> 화살을
return; return; // 중단할거에요 -> 함수를
} }
// 정밀 충돌 감지 // 정밀 충돌 감지
CheckPrecisionCollision(); CheckPrecisionCollision(); // 함수를 실행할거에요 -> 정밀 충돌 감지 함수를
previousPosition = transform.position; previousPosition = transform.position; // 값을 저장할거에요 -> 현재 위치를 이전 위치로
} }
/// <summary> /// <summary>
/// SphereCast 기반 정밀 충돌 (기존 로직 100% 유지) /// SphereCast 기반 정밀 충돌 (기존 로직 100% 유지)
/// </summary> /// </summary>
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 Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f
? rb.velocity.normalized ? rb.velocity.normalized
: transform.forward; : transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면을
float frameDistance = Vector3.Distance(previousPosition, transform.position); float frameDistance = Vector3.Distance(previousPosition, transform.position); // 거리를 계산할거에요 -> 프레임 간 이동 거리를
float checkDistance = Mathf.Max(frameDistance, raycastDistance); float checkDistance = Mathf.Max(frameDistance, raycastDistance); // 값을 결정할거에요 -> 이동 거리와 최소 거리 중 큰 값을
RaycastHit hit; RaycastHit hit; // 변수를 선언할거에요 -> 충돌 정보를 담을 hit을
bool didHit = Physics.SphereCast( bool didHit = Physics.SphereCast(
tipPosition, tipPosition,
raycastRadius, raycastRadius,
@ -150,64 +150,64 @@ public class PlayerArrow : MonoBehaviour
out hit, out hit,
checkDistance, checkDistance,
hitLayers 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을
} }
} }
/// <summary> /// <summary>
/// [MODIFIED] 충돌 처리 — 속성 데미지 적용 추가 /// [MODIFIED] 충돌 처리 — 속성 데미지 적용 추가
/// </summary> /// </summary>
private void HandleHit(Collider hitCollider, Vector3 hitPoint) private void HandleHit(Collider hitCollider, Vector3 hitPoint) // 함수를 선언할거에요 -> 충돌 결과를 처리하는 HandleHit을
{ {
if (hasHit) return; if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 충돌 처리되었다면
hasHit = true; hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로
// 적 감지 (Tag 또는 Layer) // 적 감지 (Tag 또는 Layer)
bool isEnemy = hitCollider.CompareTag("Enemy"); bool isEnemy = hitCollider.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지
// EnemyHitBox 레이어 체크 (레이어가 존재하는 경우에만) // EnemyHitBox 레이어 체크 (레이어가 존재하는 경우에만)
int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 레이어 인덱스를
if (!isEnemy && enemyHitBoxLayerIndex != -1) if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면
{ {
isEnemy = hitCollider.gameObject.layer == enemyHitBoxLayerIndex; isEnemy = hitCollider.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 해당 레이어인지 확인해서
} }
if (isEnemy) if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면
{ {
// MonsterClass를 찾아 데미지 적용 // MonsterClass를 찾아 데미지 적용
MonsterClass monster = hitCollider.GetComponentInParent<MonsterClass>(); MonsterClass monster = hitCollider.GetComponentInParent<MonsterClass>(); // 컴포넌트를 찾을거에요 -> 부모의 몬스터 클래스를
if (monster == null) monster = hitCollider.GetComponent<MonsterClass>(); if (monster == null) monster = hitCollider.GetComponent<MonsterClass>(); // 없으면 찾을거에요 -> 자신의 몬스터 클래스를
if (monster != null) if (monster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 있다면
{ {
// 1. 기본 데미지 적용 // 1. 기본 데미지 적용
monster.TakeDamage(damage); monster.TakeDamage(damage); // 실행할거에요 -> 데미지 입히기 함수를
Debug.Log($"적 명중! 기본데미지: {damage}"); Debug.Log($"적 명중! 기본데미지: {damage}"); // 로그를 출력할거에요 -> 명중 메시지를
// 2. [NEW] 속성 효과 적용 // 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}"); Debug.Log($"벽/바닥 충돌! 위치: {hitPoint}"); // 로그를 출력할거에요 -> 충돌 위치를
Destroy(gameObject, 0.05f); Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
} }
else else // 조건이 틀리면 실행할거에요 -> 그 외의 충돌이라면
{ {
// 기타 충돌 (Unknown) // 기타 충돌 (Unknown)
Destroy(gameObject, 0.05f); Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
} }
} }
@ -215,31 +215,31 @@ public class PlayerArrow : MonoBehaviour
/// [NEW] 속성 효과 적용 /// [NEW] 속성 효과 적용
/// MonsterClass에 ApplyStatusEffect 메서드가 있어야 합니다. /// MonsterClass에 ApplyStatusEffect 메서드가 있어야 합니다.
/// </summary> /// </summary>
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가 있는지 확인 // MonsterClass에 ApplyStatusEffect가 있는지 확인
switch (elementType) switch (elementType) // 분기할거에요 -> 속성 타입에 따라
{ {
case ArrowElementType.Fire: case ArrowElementType.Fire: // 조건이 맞으면 실행할거에요 -> 불 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Burn, elementDamage, elementDuration); monster.ApplyStatusEffect(StatusEffectType.Burn, elementDamage, elementDuration); // 실행할거에요 -> 화상 효과 적용을
Debug.Log($"화염 효과! {elementDamage} 데미지 x {elementDuration}초"); Debug.Log($"화염 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
break; break;
case ArrowElementType.Ice: case ArrowElementType.Ice: // 조건이 맞으면 실행할거에요 -> 얼음 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Slow, elementDamage, elementDuration); monster.ApplyStatusEffect(StatusEffectType.Slow, elementDamage, elementDuration); // 실행할거에요 -> 슬로우 효과 적용을
Debug.Log($"빙결 효과! 슬로우 {elementDuration}초"); Debug.Log($"빙결 효과! 슬로우 {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
break; break;
case ArrowElementType.Poison: case ArrowElementType.Poison: // 조건이 맞으면 실행할거에요 -> 독 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Poison, elementDamage, elementDuration); monster.ApplyStatusEffect(StatusEffectType.Poison, elementDamage, elementDuration); // 실행할거에요 -> 독 효과 적용을
Debug.Log($"독 효과! {elementDamage} 데미지 x {elementDuration}초"); Debug.Log($"독 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
break; break;
case ArrowElementType.Lightning: case ArrowElementType.Lightning: // 조건이 맞으면 실행할거에요 -> 번개 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Shock, elementDamage, elementDuration); monster.ApplyStatusEffect(StatusEffectType.Shock, elementDamage, elementDuration); // 실행할거에요 -> 감전(스턴) 효과 적용을
Debug.Log($"감전 효과! {elementDamage} 범위 데미지"); Debug.Log($"감전 효과! {elementDamage} 범위 데미지"); // 로그를 출력할거에요 -> 효과 설명을
break; break;
} }
} }
@ -247,58 +247,58 @@ public class PlayerArrow : MonoBehaviour
/// <summary> /// <summary>
/// 백업 충돌 감지 (OnTriggerEnter) — 기존 로직 유지 /// 백업 충돌 감지 (OnTriggerEnter) — 기존 로직 유지
/// </summary> /// </summary>
private void OnTriggerEnter(Collider other) private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거 충돌 시 OnTriggerEnter를
{ {
if (hasHit) return; if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 맞았다면
bool isEnemy = other.CompareTag("Enemy"); bool isEnemy = other.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지
int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 히트박스 레이어 인덱스를
if (!isEnemy && enemyHitBoxLayerIndex != -1) 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; hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로
Destroy(gameObject, 0.05f); Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
} }
} }
/// <summary> /// <summary>
/// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가 /// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가
/// </summary> /// </summary>
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 Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f
? rb.velocity.normalized ? rb.velocity.normalized
: transform.forward; : transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면으로
Gizmos.color = hasHit ? Color.red : Color.green; Gizmos.color = hasHit ? Color.red : Color.green; // 색상을 설정할거에요 -> 맞았으면 빨강, 아니면 초록으로
Gizmos.DrawWireSphere(tipPosition, raycastRadius); Gizmos.DrawWireSphere(tipPosition, raycastRadius); // 그림을 그릴거에요 -> 팁 위치에 구체를
Gizmos.DrawWireSphere(tipPosition + direction * raycastDistance, raycastRadius); Gizmos.DrawWireSphere(tipPosition + direction * raycastDistance, raycastRadius); // 그림을 그릴거에요 -> 예상 도달 위치에 구체를
Gizmos.color = Color.yellow; Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로
Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance); Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance); // 선을 그릴거에요 -> 궤적을 표시하는
// [NEW] 속성별 색상 표시 // [NEW] 속성별 색상 표시
switch (elementType) switch (elementType) // 분기할거에요 -> 속성 타입에 따라
{ {
case ArrowElementType.Fire: Gizmos.color = Color.red; break; case ArrowElementType.Fire: Gizmos.color = Color.red; break; // 색상을 바꿀거에요 -> 빨강으로
case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; // 색상을 바꿀거에요 -> 청록으로
case ArrowElementType.Poison: Gizmos.color = Color.green; break; case ArrowElementType.Poison: Gizmos.color = Color.green; break; // 색상을 바꿀거에요 -> 초록으로
case ArrowElementType.Lightning: Gizmos.color = Color.yellow; 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); // 그림을 그릴거에요 -> 속성 표시용 구체를
} }
} }
} }

View File

@ -1,46 +1,18 @@
// ============================================ using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
// 📁 파일명: ArrowData.cs
// 📂 경로: Assets/Scripts/Data/ArrowData.cs
// ============================================
using UnityEngine; [CreateAssetMenu(fileName = "New Arrow Data", menuName = "Combat/Arrow Data")] // 메뉴를 만들거에요 -> 에셋 메뉴를
public class ArrowData : ScriptableObject // 클래스를 선언할거에요 -> 데이터 컨테이너인 ArrowData를
/// <summary>
/// 화살 정보를 담는 데이터 구조체
/// ArrowPickup.GetArrowData() → PlayerAttack.SetCurrentArrow() 로 전달됩니다.
/// </summary>
[System.Serializable]
public struct ArrowData
{ {
[Header("--- 기본 정보 ---")] [Header("기본 정보")] // 인스펙터 창에 제목을 표시할거에요 -> 기본 정보 를
public string arrowName; // 화살 이름 (UI 표시용) public string arrowName; // 변수를 선언할거에요 -> 화살 이름을
public ArrowElementType elementType; // 속성 타입 public GameObject projectilePrefab; // 변수를 선언할거에요 -> 투사체 프리팹을
public Sprite icon; // 변수를 선언할거에요 -> 아이콘 이미지를
[Header("--- 스탯 ---")] [Header("데미지 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 데미지 설정 을
public float baseDamage; // 기본 데미지 public float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 물리 데미지를 baseDamage에 (이게 빠져서 오류남)
public float elementDamage; // 속성 추가 데미지
public float elementDuration; // 속성 지속시간 (초)
[Header("--- 프리팹 ---")] [Header("속성 정보")] // 인스펙터 창에 제목을 표시할거에요 -> 속성 정보 를
public GameObject projectilePrefab; // 발사할 파티클 프리팹 public ArrowElementType elementType; // 변수를 선언할거에요 -> 속성 타입을
public float elementDamage; // 변수를 선언할거에요 -> 속성 추가 데미지를
/// <summary> public float elementDuration; // 변수를 선언할거에요 -> 속성 지속 시간을
/// 기본 화살 데이터 생성
/// </summary>
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";
}
} }

View File

@ -1,102 +1,102 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
using System.Collections.Generic; using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
public class PlayerAttack : MonoBehaviour public class PlayerAttack : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerAttack을
{ {
[Header("--- 활 설정 ---")] [Header("--- 활 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 활 설정 --- 을
[SerializeField] private Transform firePoint; [SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 화살 발사 위치인 firePoint를
[SerializeField] private PlayerAnimator pAnim; [SerializeField] private PlayerAnimator pAnim; // 변수를 선언할거에요 -> 플레이어 애니메이터 스크립트인 pAnim을
[Header("--- 스탯 참조 ---")] [Header("--- 스탯 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 스탯 참조 --- 를
[SerializeField] private Stats playerStats; [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트인 playerStats를
[Header("--- 일반 공격 (좌클릭) ---")] [Header("--- 일반 공격 (좌클릭) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 일반 공격 (좌클릭) --- 을
[SerializeField] private float normalRange = 15f; [SerializeField] private float normalRange = 15f; // 변수를 선언할거에요 -> 일반 공격 사거리인 normalRange를
[SerializeField] private float normalSpeed = 20f; [SerializeField] private float normalSpeed = 20f; // 변수를 선언할거에요 -> 일반 화살 속도인 normalSpeed를
[SerializeField] private float attackCooldown = 0.5f; [SerializeField] private float attackCooldown = 0.5f; // 변수를 선언할거에요 -> 공격 쿨타임인 attackCooldown을
[Header("--- 차징 공격 (우클릭) ---")] [Header("--- 차징 공격 (우클릭) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 차징 공격 (우클릭) --- 을
[SerializeField] private float maxChargeTime = 2.0f; [SerializeField] private float maxChargeTime = 2.0f; // 변수를 선언할거에요 -> 최대 차징 시간인 maxChargeTime을
[System.Serializable] [System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정할 수 있게 구조체를
public struct ChargeStage public struct ChargeStage // 구조체를 정의할거에요 -> 차징 단계를 정의하는 ChargeStage를
{ {
public float chargeTime; public float chargeTime; // 변수를 선언할거에요 -> 도달 시간인 chargeTime을
public float damageMult; public float damageMult; // 변수를 선언할거에요 -> 데미지 배율인 damageMult를
public float rangeMult; public float rangeMult; // 변수를 선언할거에요 -> 사거리/속도 배율인 rangeMult를
} }
[SerializeField] private List<ChargeStage> chargeStages; [SerializeField] private List<ChargeStage> chargeStages; // 리스트를 선언할거에요 -> 차징 단계 목록인 chargeStages를
[Header("--- 에임 보정 설정 ---")] [Header("--- 에임 보정 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 에임 보정 설정 --- 을
[SerializeField] private bool enableAutoAim = true; [SerializeField] private bool enableAutoAim = true; // 변수를 선언할거에요 -> 자동 조준 사용 여부인 enableAutoAim을
[SerializeField] private float autoAimRange = 15f; [SerializeField] private float autoAimRange = 15f; // 변수를 선언할거에요 -> 자동 조준 감지 거리인 autoAimRange를
[SerializeField] private float autoAimAngle = 45f; [SerializeField] private float autoAimAngle = 45f; // 변수를 선언할거에요 -> 자동 조준 감지 각도인 autoAimAngle을
[Range(0f, 1f)] [Range(0f, 1f)] // 슬라이더를 만들거에요 -> 0부터 1 사이의 값으로
[SerializeField] private float aimAssistStrength = 0.4f; [SerializeField] private float aimAssistStrength = 0.4f; // 변수를 선언할거에요 -> 보정 강도인 aimAssistStrength를
[SerializeField] private LayerMask enemyLayer; [SerializeField] private LayerMask enemyLayer; // 변수를 선언할거에요 -> 적 레이어인 enemyLayer를
[SerializeField] private bool onlyMaxCharge = true; [SerializeField] private bool onlyMaxCharge = true; // 변수를 선언할거에요 -> 풀차징 때만 유도할지 여부인 onlyMaxCharge를
// ============================================ // ============================================
// [NEW] 파티클 기반 화살 시스템 // [NEW] 파티클 기반 화살 시스템
// ============================================ // ============================================
[Header("--- 파티클 화살 시스템 ---")] [Header("--- 파티클 화살 시스템 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 파티클 화살 시스템 --- 을
[SerializeField] private GameObject defaultProjectilePrefab; // 기본 발사체 프리팹 [SerializeField] private GameObject defaultProjectilePrefab; // 변수를 선언할거에요 -> 기본 발사체 프리팹인 defaultProjectilePrefab을
private GameObject _currentProjectilePrefab; private GameObject _currentProjectilePrefab; // 변수를 선언할거에요 -> 현재 사용할 발사체 프리팹인 _currentProjectilePrefab을
private ArrowElementType _currentElementType = ArrowElementType.None; private ArrowElementType _currentElementType = ArrowElementType.None; // 변수를 초기화할거에요 -> 현재 화살 속성인 _currentElementType을 없음으로
private float _currentElementDamage = 0f; private float _currentElementDamage = 0f; // 변수를 초기화할거에요 -> 현재 속성 데미지인 _currentElementDamage를 0으로
private float _currentElementDuration = 0f; private float _currentElementDuration = 0f; // 변수를 초기화할거에요 -> 현재 속성 지속 시간인 _currentElementDuration을 0으로
private float _lastAttackTime; private float _lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간인 _lastAttackTime을
private float _chargeTimer; private float _chargeTimer; // 변수를 선언할거에요 -> 차징 진행 시간인 _chargeTimer를
private bool _isCharging = false; private bool _isCharging = false; // 변수를 초기화할거에요 -> 차징 중 여부인 _isCharging을 거짓으로
private bool _isAttacking = false; private bool _isAttacking = false; // 변수를 초기화할거에요 -> 공격 동작 중 여부인 _isAttacking을 거짓으로
private bool _waitForRelease = false; private bool _waitForRelease = false; // 변수를 초기화할거에요 -> 발사 대기 상태 여부인 _waitForRelease를 거짓으로
private float _pendingDamage; private float _pendingDamage; // 변수를 선언할거에요 -> 발사될 화살의 데미지인 _pendingDamage를
private float _pendingSpeed; private float _pendingSpeed; // 변수를 선언할거에요 -> 발사될 화살의 속도인 _pendingSpeed를
private float _pendingRange; private float _pendingRange; // 변수를 선언할거에요 -> 발사될 화살의 사거리인 _pendingRange를
private Vector3 _pendingShootDirection; private Vector3 _pendingShootDirection; // 변수를 선언할거에요 -> 발사될 방향인 _pendingShootDirection을
public bool IsCharging => _isCharging; public bool IsCharging => _isCharging; // 프로퍼티를 선언할거에요 -> 차징 중인지 여부를 반환하는 IsCharging을
public bool IsAttacking { get => _isAttacking; set => _isAttacking = value; } public bool IsAttacking { get => _isAttacking; set => _isAttacking = value; } // 프로퍼티를 선언할거에요 -> 공격 중인지 여부를 읽고 쓰는 IsAttacking을
public float ChargeProgress => Mathf.Clamp01(_chargeTimer / maxChargeTime); public float ChargeProgress => Mathf.Clamp01(_chargeTimer / maxChargeTime); // 프로퍼티를 선언할거에요 -> 차징 진행률(0~1)을 반환하는 ChargeProgress를
// [NEW] 외부에서 현재 속성 확인용 // [NEW] 외부에서 현재 속성 확인용
public ArrowElementType CurrentElement => _currentElementType; public ArrowElementType CurrentElement => _currentElementType; // 프로퍼티를 선언할거에요 -> 현재 화살 속성을 반환하는 CurrentElement를
public GameObject CurrentProjectilePrefab => _currentProjectilePrefab; public GameObject CurrentProjectilePrefab => _currentProjectilePrefab; // 프로퍼티를 선언할거에요 -> 현재 발사체 프리팹을 반환하는 CurrentProjectilePrefab을
private void Start() private void Start() // 함수를 실행할거에요 -> 스크립트 시작 시 Start를
{ {
if (playerStats == null) playerStats = GetComponentInParent<Stats>(); if (playerStats == null) playerStats = GetComponentInParent<Stats>(); // 조건이 맞으면 가져올거에요 -> 부모의 Stats 컴포넌트를 playerStats에
// 기본 발사체 프리팹 설정 // 기본 발사체 프리팹 설정
if (_currentProjectilePrefab == null) if (_currentProjectilePrefab == null) // 조건이 맞으면 실행할거에요 -> 현재 설정된 프리팹이 없다면
_currentProjectilePrefab = defaultProjectilePrefab; _currentProjectilePrefab = defaultProjectilePrefab; // 값을 넣을거에요 -> 기본 프리팹을
if (chargeStages == null || chargeStages.Count == 0) if (chargeStages == null || chargeStages.Count == 0) // 조건이 맞으면 실행할거에요 -> 차징 단계 리스트가 비어있다면
{ {
chargeStages = new List<ChargeStage> chargeStages = new List<ChargeStage> // 리스트를 생성할거에요 -> 기본 3단계 차징 설정을 담은 리스트를
{ {
new ChargeStage { chargeTime = 0f, damageMult = 1f, rangeMult = 1f }, new ChargeStage { chargeTime = 0f, damageMult = 1f, rangeMult = 1f }, // 값을 넣을거에요 -> 0단계 설정을
new ChargeStage { chargeTime = 1f, damageMult = 1.5f, rangeMult = 1.2f }, new ChargeStage { chargeTime = 1f, damageMult = 1.5f, rangeMult = 1.2f }, // 값을 넣을거에요 -> 1단계 설정을
new ChargeStage { chargeTime = 2f, damageMult = 2.5f, rangeMult = 1.5f } new ChargeStage { chargeTime = 2f, damageMult = 2.5f, rangeMult = 1.5f } // 값을 넣을거에요 -> 2단계 설정을
}; };
} }
float calculatedMaxTime = 0f; float calculatedMaxTime = 0f; // 변수를 초기화할거에요 -> 계산된 최대 시간을 0으로
foreach (var stage in chargeStages) 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에서 습득 시 호출합니다. /// ArrowPickup에서 습득 시 호출합니다.
/// 파티클 프리팹 + 속성 정보를 저장합니다. /// 파티클 프리팹 + 속성 정보를 저장합니다.
/// </summary> /// </summary>
public void SetCurrentArrow(ArrowData data) public void SetCurrentArrow(ArrowData data) // 함수를 선언할거에요 -> 새로운 화살 데이터를 적용하는 SetCurrentArrow를
{ {
if (data.projectilePrefab != null) if (data.projectilePrefab != null) // 조건이 맞으면 실행할거에요 -> 데이터에 프리팹이 있다면
_currentProjectilePrefab = data.projectilePrefab; _currentProjectilePrefab = data.projectilePrefab; // 값을 바꿀거에요 -> 현재 발사체 프리팹을 새 것으로
else else // 조건이 틀리면 실행할거에요 -> 데이터에 프리팹이 없다면
_currentProjectilePrefab = defaultProjectilePrefab; _currentProjectilePrefab = defaultProjectilePrefab; // 값을 바꿀거에요 -> 기본 프리팹으로
_currentElementType = data.elementType; _currentElementType = data.elementType; // 값을 저장할거에요 -> 화살 속성 타입을
_currentElementDamage = data.elementDamage; _currentElementDamage = data.elementDamage; // 값을 저장할거에요 -> 속성 데미지를
_currentElementDuration = data.elementDuration; _currentElementDuration = data.elementDuration; // 값을 저장할거에요 -> 속성 지속 시간을
Debug.Log($"화살 장착: [{data.arrowName}] 속성={data.elementType}, " + Debug.Log($"화살 장착: [{data.arrowName}] 속성={data.elementType}, " +
$"속성데미지={data.elementDamage}, 지속시간={data.elementDuration}s"); $"속성데미지={data.elementDamage}, 지속시간={data.elementDuration}s"); // 로그를 출력할거에요 -> 장착된 화살 정보를
} }
/// <summary> /// <summary>
/// 기본 화살로 초기화 /// 기본 화살로 초기화
/// </summary> /// </summary>
public void ResetArrow() public void ResetArrow() // 함수를 선언할거에요 -> 화살을 기본 상태로 되돌리는 ResetArrow를
{ {
_currentProjectilePrefab = defaultProjectilePrefab; _currentProjectilePrefab = defaultProjectilePrefab; // 값을 바꿀거에요 -> 발사체를 기본 프리팹으로
_currentElementType = ArrowElementType.None; _currentElementType = ArrowElementType.None; // 값을 바꿀거에요 -> 속성을 없음(None)으로
_currentElementDamage = 0f; _currentElementDamage = 0f; // 값을 초기화할거에요 -> 속성 데미지를 0으로
_currentElementDuration = 0f; _currentElementDuration = 0f; // 값을 초기화할거에요 -> 속성 시간을 0으로
Debug.Log("화살 초기화: 기본 화살"); Debug.Log("화살 초기화: 기본 화살"); // 로그를 출력할거에요 -> 초기화 완료 메시지를
} }
// ============================================ // ============================================
// 일반 공격 (좌클릭) — 기존 로직 100% 유지 // 일반 공격 (좌클릭) — 기존 로직 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; _pendingDamage = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; // 값을 가져올거에요 -> 스탯이 있으면 총 공격력을, 없으면 10을
_pendingSpeed = normalSpeed; _pendingSpeed = normalSpeed; // 값을 저장할거에요 -> 기본 속도를
_pendingRange = normalRange; _pendingRange = normalRange; // 값을 저장할거에요 -> 기본 사거리를
_pendingShootDirection = GetMouseDirection(); _pendingShootDirection = GetMouseDirection(); // 값을 계산할거에요 -> 마우스 방향을 구해서 발사 방향으로
_lastAttackTime = Time.time; _lastAttackTime = Time.time; // 값을 갱신할거에요 -> 마지막 공격 시간을 현재로
if (pAnim != null) pAnim.TriggerThrow(); if (pAnim != null) pAnim.TriggerThrow(); // 실행할거에요 -> 던지는 애니메이션 트리거를
StartCoroutine(AttackRoutine()); StartCoroutine(AttackRoutine()); // 코루틴을 시작할거에요 -> 공격 상태 관리를 위한 AttackRoutine을
} }
// ============================================ // ============================================
// 차징 공격 (우클릭) — 기존 로직 100% 유지 // 차징 공격 (우클릭) — 기존 로직 100% 유지
// ============================================ // ============================================
public void StartCharging() public void StartCharging() // 함수를 선언할거에요 -> 차징을 시작하는 StartCharging을
{ {
if (_waitForRelease) return; if (_waitForRelease) return; // 조건이 맞으면 중단할거에요 -> 이미 발사 대기 중이라면
_isCharging = true; _isCharging = true; // 상태를 바꿀거에요 -> 차징 중 상태를 참(true)으로
_chargeTimer = 0f; _chargeTimer = 0f; // 값을 초기화할거에요 -> 차징 타이머를 0으로
if (pAnim != null) pAnim.SetCharging(true); if (pAnim != null) pAnim.SetCharging(true); // 실행할거에요 -> 애니메이터에 차징 시작을 알리기를
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true); if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true); // 조건이 맞으면 실행할거에요 -> 카메라 줌 효과를 켜기를
} }
private void ResetChargingEffects() private void ResetChargingEffects() // 함수를 선언할거에요 -> 차징 효과를 초기화하는 ResetChargingEffects를
{ {
_isCharging = false; _isCharging = false; // 상태를 바꿀거에요 -> 차징 상태를 거짓(false)으로
_chargeTimer = 0f; _chargeTimer = 0f; // 값을 초기화할거에요 -> 차징 타이머를
if (pAnim != null) pAnim.SetCharging(false); if (pAnim != null) pAnim.SetCharging(false); // 실행할거에요 -> 애니메이터 차징 상태 끄기를
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false); if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false); // 조건이 맞으면 실행할거에요 -> 카메라 줌 효과를 끄기를
} }
public void CancelCharging() public void CancelCharging() // 함수를 선언할거에요 -> 차징을 취소하는 CancelCharging을
{ {
ResetChargingEffects(); ResetChargingEffects(); // 함수를 실행할거에요 -> 차징 효과 초기화를
_waitForRelease = false; _waitForRelease = false; // 상태를 바꿀거에요 -> 발사 대기 상태를 거짓으로
} }
public void ReleaseAttack() public void ReleaseAttack() // 함수를 선언할거에요 -> 차징된 공격을 발사하는 ReleaseAttack을
{ {
if (!_isCharging) return; if (!_isCharging) return; // 조건이 맞으면 중단할거에요 -> 차징 중이 아니라면
ChargeStage currentStage = chargeStages[0]; ChargeStage currentStage = chargeStages[0]; // 변수를 초기화할거에요 -> 기본 0단계를 시작값으로
foreach (var stage in chargeStages) foreach (var stage in chargeStages) // 반복할거에요 -> 모든 단계를 돌면서
{ {
if (_chargeTimer >= stage.chargeTime) currentStage = stage; if (_chargeTimer >= stage.chargeTime) currentStage = stage; // 조건이 맞으면 갱신할거에요 -> 타이머가 단계 시간보다 길다면 해당 단계로
} }
float baseDmg = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; float baseDmg = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; // 값을 가져올거에요 -> 기본 공격력을
_pendingDamage = baseDmg * currentStage.damageMult; _pendingDamage = baseDmg * currentStage.damageMult; // 값을 계산할거에요 -> 배율을 적용한 최종 데미지를
_pendingSpeed = normalSpeed * currentStage.rangeMult; _pendingSpeed = normalSpeed * currentStage.rangeMult; // 값을 계산할거에요 -> 배율을 적용한 속도를
_pendingRange = normalRange * currentStage.rangeMult; _pendingRange = normalRange * currentStage.rangeMult; // 값을 계산할거에요 -> 배율을 적용한 사거리를
bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f; bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f; // 조건을 확인할거에요 -> 풀차징(95% 이상)인지 여부를
_pendingShootDirection = GetShootDirection(isMaxCharge); _pendingShootDirection = GetShootDirection(isMaxCharge); // 값을 계산할거에요 -> 보정이 적용된 발사 방향을
if (pAnim != null) pAnim.TriggerThrow(); if (pAnim != null) pAnim.TriggerThrow(); // 실행할거에요 -> 던지는 애니메이션을
_waitForRelease = true; _waitForRelease = true; // 상태를 바꿀거에요 -> 발사 대기 상태를 참으로
_lastAttackTime = Time.time; _lastAttackTime = Time.time; // 값을 갱신할거에요 -> 마지막 공격 시간을
StartCoroutine(AttackRoutine()); StartCoroutine(AttackRoutine()); // 코루틴을 시작할거에요 -> 공격 루틴을
} }
// ============================================ // ============================================
// 에임 보정 시스템 — 기존 로직 100% 유지 // 에임 보정 시스템 — 기존 로직 100% 유지
// ============================================ // ============================================
private Vector3 GetMouseDirection() private Vector3 GetMouseDirection() // 함수를 선언할거에요 -> 마우스가 가리키는 방향을 구하는 GetMouseDirection을
{ {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 레이를 쏠거에요 -> 카메라에서 마우스 위치로
Plane firePlane = new Plane(Vector3.up, firePoint.position); 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 worldMousePos = ray.GetPoint(distance); // 위치를 구할거에요 -> 평면상 마우스 위치를
Vector3 direction = (worldMousePos - firePoint.position).normalized; Vector3 direction = (worldMousePos - firePoint.position).normalized; // 벡터를 구할거에요 -> 발사 위치에서 마우스까지의 방향을
direction.y = 0; direction.y = 0; // 값을 바꿀거에요 -> 높이 차이는 무시하게 y를 0으로
return direction; return direction; // 반환할거에요 -> 계산된 방향을
} }
return transform.forward; return transform.forward; // 반환할거에요 -> 실패 시 정면 방향을
} }
private Vector3 GetShootDirection(bool isMaxCharge) private Vector3 GetShootDirection(bool isMaxCharge) // 함수를 선언할거에요 -> 최종 발사 방향(보정 포함)을 구하는 GetShootDirection을
{ {
Vector3 mouseDir = GetMouseDirection(); Vector3 mouseDir = GetMouseDirection(); // 값을 가져올거에요 -> 마우스 방향을
if (!enableAutoAim || (onlyMaxCharge && !isMaxCharge)) return mouseDir; if (!enableAutoAim || (onlyMaxCharge && !isMaxCharge)) return mouseDir; // 조건이 맞으면 반환할거에요 -> 보정 안 씀 설정이거나 풀차징이 아니면 마우스 방향 그대로
Transform bestTarget = FindBestTarget(mouseDir); Transform bestTarget = FindBestTarget(mouseDir); // 함수를 실행할거에요 -> 가장 적합한 타겟을 찾는 FindBestTarget을
if (bestTarget != null) if (bestTarget != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면
{ {
Vector3 targetPos = bestTarget.position + Vector3.up * 1.2f; Vector3 targetPos = bestTarget.position + Vector3.up * 1.2f; // 위치를 계산할거에요 -> 타겟의 중심부(높이 보정)를
Vector3 targetDir = (targetPos - firePoint.position).normalized; Vector3 targetDir = (targetPos - firePoint.position).normalized; // 방향을 계산할거에요 -> 발사 위치에서 타겟까지
targetDir.y = 0; targetDir.y = 0; // 값을 바꿀거에요 -> 수평 방향만 고려하게
Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); // 보간할거에요 -> 마우스 방향과 타겟 방향 사이를 강도만큼
assistedDir.Normalize(); assistedDir.Normalize(); // 정규화할거에요 -> 벡터 길이를 1로
return assistedDir; 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); Collider[] enemies = Physics.OverlapSphere(transform.position, autoAimRange, enemyLayer); // 배열에 담을거에요 -> 사거리 내의 모든 적을
if (enemies.Length == 0) return null; if (enemies.Length == 0) return null; // 조건이 맞으면 반환할거에요 -> 적이 없으면 null을
Transform bestTarget = null; Transform bestTarget = null; // 변수를 초기화할거에요 -> 최고 타겟을 비워두고
float bestScore = float.MaxValue; float bestScore = float.MaxValue; // 변수를 초기화할거에요 -> 점수를 최대값으로
foreach (var enemy in enemies) foreach (var enemy in enemies) // 반복할거에요 -> 모든 적에 대해
{ {
Vector3 dirToEnemy = (enemy.transform.position - transform.position).normalized; Vector3 dirToEnemy = (enemy.transform.position - transform.position).normalized; // 방향을 구할거에요 -> 적을 향하는 방향을
float angle = Vector3.Angle(mouseDirection, dirToEnemy); float angle = Vector3.Angle(mouseDirection, dirToEnemy); // 각도를 잴거에요 -> 마우스 방향과 적 방향 사이를
if (angle > autoAimAngle) continue; if (angle > autoAimAngle) continue; // 조건이 맞으면 건너뛸거에요 -> 시야각 밖이라면
float distance = Vector3.Distance(transform.position, enemy.transform.position); float distance = Vector3.Distance(transform.position, enemy.transform.position); // 거리를 잴거에요 -> 적까지의 거리를
float score = angle * 0.5f + distance * 0.5f; float score = angle * 0.5f + distance * 0.5f; // 점수를 매길거에요 -> 각도와 거리를 합산해서 (낮을수록 좋음)
if (score < bestScore) if (score < bestScore) // 조건이 맞으면 갱신할거에요 -> 현재 점수가 최고 점수보다 낮다면
{ {
bestScore = score; bestScore = score; // 값을 저장할거에요 -> 새 점수를
bestTarget = enemy.transform; bestTarget = enemy.transform; // 값을 저장할거에요 -> 새 타겟을
} }
} }
return bestTarget; return bestTarget; // 반환할거에요 -> 가장 점수가 좋은 타겟을
} }
// ============================================ // ============================================
@ -276,27 +276,27 @@ public class PlayerAttack : MonoBehaviour
/// 애니메이션 이벤트에서 호출됩니다. /// 애니메이션 이벤트에서 호출됩니다.
/// 파티클 프리팹을 Instantiate하고 PlayerArrow 컴포넌트로 이동/충돌을 처리합니다. /// 파티클 프리팹을 Instantiate하고 PlayerArrow 컴포넌트로 이동/충돌을 처리합니다.
/// </summary> /// </summary>
public void OnShootArrow() public void OnShootArrow() // 함수를 선언할거에요 -> 애니메이션 이벤트로 호출될 발사 함수 OnShootArrow를
{ {
if (_currentProjectilePrefab == null || firePoint == null) if (_currentProjectilePrefab == null || firePoint == null) // 조건이 맞으면 중단할거에요 -> 프리팹이나 발사 위치가 없다면
{ {
Debug.LogWarning("발사체 프리팹 또는 firePoint가 없습니다!"); Debug.LogWarning("발사체 프리팹 또는 firePoint가 없습니다!"); // 경고 로그를 띄울거에요 -> 설정 확인 필요 메시지를
return; return;
} }
// 파티클 프리팹을 발사 위치에 생성 // 파티클 프리팹을 발사 위치에 생성
Quaternion rotation = Quaternion.LookRotation(_pendingShootDirection); Quaternion rotation = Quaternion.LookRotation(_pendingShootDirection); // 회전을 계산할거에요 -> 발사 방향을 바라보게
GameObject projectile = Instantiate(_currentProjectilePrefab, firePoint.position, rotation); GameObject projectile = Instantiate(_currentProjectilePrefab, firePoint.position, rotation); // 생성할거에요 -> 화살 오브젝트를
// PlayerArrow 컴포넌트 확인/추가 후 초기화 // PlayerArrow 컴포넌트 확인/추가 후 초기화
PlayerArrow arrowScript = projectile.GetComponent<PlayerArrow>(); PlayerArrow arrowScript = projectile.GetComponent<PlayerArrow>(); // 컴포넌트를 가져올거에요 -> PlayerArrow 스크립트를
if (arrowScript == null) if (arrowScript == null) // 조건이 맞으면 실행할거에요 -> 스크립트가 없다면
{ {
arrowScript = projectile.AddComponent<PlayerArrow>(); arrowScript = projectile.AddComponent<PlayerArrow>(); // 추가할거에요 -> PlayerArrow 컴포넌트를 즉석에서
} }
// 발사 정보 + 속성 정보 전달 // 발사 정보 + 속성 정보 전달
arrowScript.Initialize( arrowScript.Initialize( // 초기화할거에요 -> 화살 스크립트에 모든 정보를 전달해서
_pendingDamage, _pendingDamage,
_pendingSpeed, _pendingSpeed,
_pendingRange, _pendingRange,
@ -311,63 +311,63 @@ public class PlayerAttack : MonoBehaviour
// 유틸리티 — 기존 로직 100% 유지 // 유틸리티 — 기존 로직 100% 유지
// ============================================ // ============================================
public void OnAttackEnd() public void OnAttackEnd() // 함수를 선언할거에요 -> 공격 애니메이션 종료 시 호출될 OnAttackEnd를
{ {
_isAttacking = false; _isAttacking = false; // 상태를 바꿀거에요 -> 공격 상태를 거짓으로
ResetChargingEffects(); ResetChargingEffects(); // 함수를 실행할거에요 -> 차징 효과 초기화를
} }
private IEnumerator AttackRoutine() private IEnumerator AttackRoutine() // 코루틴 함수를 선언할거에요 -> 공격 상태 안전장치인 AttackRoutine을
{ {
_isAttacking = true; _isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참으로
yield return new WaitForSeconds(0.6f); yield return new WaitForSeconds(0.6f); // 기다릴거에요 -> 0.6초(모션 시간)만큼
if (_isAttacking) if (_isAttacking) // 조건이 맞으면 실행할거에요 -> 아직도 공격 중 상태라면 (강제 종료)
{ {
_isAttacking = false; _isAttacking = false; // 상태를 바꿀거에요 -> 공격 상태를 거짓으로
ResetChargingEffects(); ResetChargingEffects(); // 함수를 실행할거에요 -> 효과 초기화를
} }
} }
// [DEPRECATED] 하위 호환성을 위해 남겨둠 // [DEPRECATED] 하위 호환성을 위해 남겨둠
[System.Obsolete("SetCurrentArrow(ArrowData)를 사용하세요.")] [System.Obsolete("SetCurrentArrow(ArrowData)를 사용하세요.")] // 경고를 띄울거에요 -> 이 함수는 더 이상 쓰지 말라고
public void SwapArrow(GameObject newArrow) public void SwapArrow(GameObject newArrow) // 함수를 선언할거에요 -> 구버전 화살 교체 함수 SwapArrow를
{ {
if (newArrow == null) return; if (newArrow == null) return; // 조건이 맞으면 중단할거에요 -> 비어있다면
Debug.LogWarning("SwapArrow()는 더 이상 사용되지 않습니다. SetCurrentArrow()를 사용하세요."); Debug.LogWarning("SwapArrow()는 더 이상 사용되지 않습니다. SetCurrentArrow()를 사용하세요."); // 경고 로그를 출력할거에요 -> 새 함수 사용 권장을
} }
public void StartWeaponCollision() { } public void StartWeaponCollision() { } // 함수를 비워둘거에요 -> 근접 무기 충돌 시작(활에는 필요 없음)
public void StopWeaponCollision() { } public void StopWeaponCollision() { } // 함수를 비워둘거에요 -> 근접 무기 충돌 종료(활에는 필요 없음)
// ============================================ // ============================================
// Gizmo 디버그 — 기존 로직 100% 유지 // Gizmo 디버그 — 기존 로직 100% 유지
// ============================================ // ============================================
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터 선택 시 디버그 그림을 그리는 OnDrawGizmosSelected를
{ {
if (!enableAutoAim) return; if (!enableAutoAim) return; // 조건이 맞으면 중단할거에요 -> 자동 조준이 꺼져있다면
Gizmos.color = Color.yellow; Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로
Gizmos.DrawWireSphere(transform.position, autoAimRange); Gizmos.DrawWireSphere(transform.position, autoAimRange); // 그림을 그릴거에요 -> 자동 조준 범위를
if (Application.isPlaying && firePoint != null) if (Application.isPlaying && firePoint != null) // 조건이 맞으면 실행할거에요 -> 게임 실행 중이고 발사 위치가 있다면
{ {
Vector3 mouseDir = GetMouseDirection(); Vector3 mouseDir = GetMouseDirection(); // 값을 가져올거에요 -> 마우스 방향을
Gizmos.color = Color.blue; Gizmos.color = Color.blue; // 색상을 설정할거에요 -> 파란색으로
Gizmos.DrawRay(firePoint.position, mouseDir * 5f); Gizmos.DrawRay(firePoint.position, mouseDir * 5f); // 선을 그릴거에요 -> 마우스 방향 레이를
Transform target = FindBestTarget(mouseDir); Transform target = FindBestTarget(mouseDir); // 값을 가져올거에요 -> 타겟을
if (target != null) if (target != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면
{ {
Vector3 targetPos = target.position + Vector3.up * 1.2f; Vector3 targetPos = target.position + Vector3.up * 1.2f; // 위치를 계산할거에요 -> 타겟 중심점을
Vector3 targetDir = (targetPos - firePoint.position).normalized; Vector3 targetDir = (targetPos - firePoint.position).normalized; // 방향을 계산할거에요 -> 타겟 방향을
targetDir.y = 0; targetDir.y = 0; // 값을 바꿀거에요 -> 수평으로
Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); // 보간할거에요 -> 보정된 방향을
assistedDir.Normalize(); assistedDir.Normalize(); // 정규화할거에요 -> 벡터를
Gizmos.color = Color.red; Gizmos.color = Color.red; // 색상을 설정할거에요 -> 빨간색으로
Gizmos.DrawRay(firePoint.position, assistedDir * 7f); Gizmos.DrawRay(firePoint.position, assistedDir * 7f); // 선을 그릴거에요 -> 최종 발사 방향을
Gizmos.color = Color.green; Gizmos.color = Color.green; // 색상을 설정할거에요 -> 초록색으로
Gizmos.DrawWireSphere(targetPos, 0.3f); Gizmos.DrawWireSphere(targetPos, 0.3f); // 그림을 그릴거에요 -> 타겟 위치 표시를
} }
} }
} }

View File

@ -9,6 +9,7 @@
/// </summary> /// </summary>
public enum StatusEffectType public enum StatusEffectType
{ {
None, // ⭐ [추가됨] 아무 상태이상 없음 (기본값)
Burn, // 화상 - 일정 시간 동안 틱 데미지 Burn, // 화상 - 일정 시간 동안 틱 데미지
Slow, // 슬로우 - 이동속도 감소 Slow, // 슬로우 - 이동속도 감소
Poison, // 중독 - 틱 데미지 + 방어력 감소 Poison, // 중독 - 틱 데미지 + 방어력 감소

View File

@ -1,228 +1,229 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
/// <summary> /// <summary> // 요약 주석을 시작할거에요 -> 클래스 설명을
/// 바닥에 떨어진 화살 아이템 (아이템 전용 — 발사체 기능 제거됨) /// 바닥에 떨어진 화살 아이템 (아이템 전용 — 발사체 기능 제거됨) // 설명할거에요 -> 이 스크립트 역할을
/// 습득 시 PlayerAttack에 파티클 프리팹 + 속성 정보를 전달합니다. /// 습득 시 PlayerAttack에 파티클 프리팹 + 속성 정보를 전달합니다. // 설명할거에요 -> 습득 시 전달 내용
/// </summary> /// </summary> // 요약 주석을 끝낼거에요 -> 클래스 설명을
public class ArrowPickup : MonoBehaviour public class ArrowPickup : MonoBehaviour // 클래스를 선언할거에요 -> 화살 아이템 습득/표시를 담당하는 ArrowPickup을
{ { // 코드 블록을 시작할거에요 -> ArrowPickup 범위를
[Header("--- 화살 아이템 정보 ---")]
[SerializeField] private string arrowName = "기본 화살";
[SerializeField] private ArrowElementType elementType = ArrowElementType.None;
[Header("--- 스탯 ---")] [Header("--- 화살 아이템 정보 ---")] // 인스펙터에 제목을 표시할거에요 -> 화살 아이템 정보 섹션을
[SerializeField] private float baseDamage = 15f; [SerializeField] private string arrowName = "기본 화살"; // 변수를 선언할거에요 -> 화살 이름을 arrowName에
[SerializeField] private float elementDamage = 5f; [SerializeField] private ArrowElementType elementType = ArrowElementType.None; // 변수를 선언할거에요 -> 화살 속성 타입을 elementType에
[SerializeField] private float elementDuration = 3f;
[Header("--- 발사체 파티클 ---")] [Header("--- 스탯 ---")] // 인스펙터에 제목을 표시할거에요 -> 스탯 섹션을
[Tooltip("이 화살이 장착될 때 발사할 파티클 프리팹")] [SerializeField] private float baseDamage = 15f; // 변수를 선언할거에요 -> 기본 데미지를 baseDamage에
[SerializeField] private GameObject projectilePrefab; [SerializeField] private float elementDamage = 5f; // 변수를 선언할거에요 -> 속성 데미지를 elementDamage에
[SerializeField] private float elementDuration = 3f; // 변수를 선언할거에요 -> 속성 지속시간을 elementDuration에
[Header("--- 비주얼 (필드 아이템) ---")] [Header("--- 발사체 파티클 ---")] // 인스펙터에 제목을 표시할거에요 -> 파티클 섹션을
[SerializeField] private GameObject visualModel; [Tooltip("이 화살이 장착될 때 발사할 파티클 프리팹")] // 인스펙터에 툴팁을 달거에요 -> projectilePrefab 설명을
[SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 장착 시 쓸 파티클 프리팹을 projectilePrefab에
[Header("--- UI 표시 ---")] [Header("--- 비주얼 (필드 아이템) ---")] // 인스펙터에 제목을 표시할거에요 -> 비주얼 섹션을
[SerializeField] private GameObject pickupUI; [SerializeField] private GameObject visualModel; // 변수를 선언할거에요 -> 필드에서 보일 모델을 visualModel에
[SerializeField] private float uiDisplayRange = 3f;
[Header("--- Collider 설정 (자동 탐지) ---")] [Header("--- UI 표시 ---")] // 인스펙터에 제목을 표시할거에요 -> UI 표시 섹션을
[Tooltip("아이템 습득용 Sphere Collider (Trigger)")] [SerializeField] private GameObject pickupUI; // 변수를 선언할거에요 -> 습득 UI 오브젝트를 pickupUI에
[SerializeField] private Collider pickupCollider; [SerializeField] private float uiDisplayRange = 3f; // 변수를 선언할거에요 -> UI 표시 거리를 uiDisplayRange에
[Tooltip("바닥 충돌용 Box Collider (물리)")]
[SerializeField] private Collider physicsCollider;
private Transform playerTransform; [Header("--- Collider 설정 (자동 탐지) ---")] // 인스펙터에 제목을 표시할거에요 -> 콜라이더 설정 섹션을
private Rigidbody rb; [Tooltip("아이템 습득용 Sphere Collider (Trigger)")] // 인스펙터에 툴팁을 달거에요 -> pickupCollider 설명을
[SerializeField] private Collider pickupCollider; // 변수를 선언할거에요 -> 습득용 트리거 콜라이더를 pickupCollider에
[Tooltip("바닥 충돌용 Box Collider (물리)")] // 인스펙터에 툴팁을 달거에요 -> physicsCollider 설명을
[SerializeField] private Collider physicsCollider; // 변수를 선언할거에요 -> 물리 충돌 콜라이더를 physicsCollider에
/// <summary> private Transform playerTransform; // 변수를 선언할거에요 -> 플레이어 트랜스폼을 저장할 playerTransform을
/// [NEW] ArrowData 구조체로 정보 패키징 private Rigidbody rb; // 변수를 선언할거에요 -> 리지드바디를 저장할 rb를
/// </summary>
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 void Awake() /// <summary> // 요약 주석을 시작할거에요 -> ArrowData 패키징 설명을
{ /// [NEW] ArrowData 구조체로 정보 패키징 // 설명할거에요 -> PlayerAttack에 넘기기 쉽게 묶는 함수
rb = GetComponent<Rigidbody>(); /// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
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) private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를
{ { // 코드 블록을 시작할거에요 -> Awake 범위를
AutoDetectColliders(); rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> Rigidbody를 찾아 rb에 저장
}
}
/// <summary> if (pickupCollider == null || physicsCollider == null) // 조건을 검사할거에요 -> 콜라이더 참조가 비어있는지
/// Collider 자동 탐지 (기존 로직 유지) { // 코드 블록을 시작할거에요 -> 자동 탐지 처리
/// </summary> AutoDetectColliders(); // 함수를 실행할거에요 -> 콜라이더 자동 탐지
private void AutoDetectColliders() } // 코드 블록을 끝낼거에요 -> 자동 탐지 처리
{ } // 코드 블록을 끝낼거에요 -> Awake를
Collider[] allColliders = GetComponents<Collider>();
foreach (Collider col in allColliders) /// <summary> // 요약 주석을 시작할거에요 -> 콜라이더 자동 탐지 설명을
{ /// Collider 자동 탐지 (기존 로직 유지) // 설명할거에요 -> Sphere/Box를 찾아 자동 세팅
if (col is SphereCollider && pickupCollider == null) /// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
{ private void AutoDetectColliders() // 함수를 선언할거에요 -> 콜라이더를 자동으로 찾는 AutoDetectColliders를
pickupCollider = col; { // 코드 블록을 시작할거에요 -> AutoDetectColliders 범위를
Debug.Log($"습득용 Collider: {col.GetType().Name}"); Collider[] allColliders = GetComponents<Collider>(); // 배열을 가져올거에요 -> 내 오브젝트의 모든 Collider를
}
else if (col is BoxCollider && physicsCollider == null)
{
physicsCollider = col;
Debug.Log($"물리용 Collider: {col.GetType().Name}");
}
}
}
private void Start() foreach (Collider col in allColliders) // 반복할거에요 -> 모든 콜라이더를 하나씩
{ { // 코드 블록을 시작할거에요 -> 콜라이더 판별 범위
SetupAsItem(); 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를
/// <summary> private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를
/// 아이템 모드 설정 (기존 로직 유지 + 단순화) { // 코드 블록을 시작할거에요 -> Start 범위를
/// </summary> SetupAsItem(); // 함수를 실행할거에요 -> 아이템 모드로 세팅
private void SetupAsItem() } // 코드 블록을 끝낼거에요 -> Start를
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
playerTransform = player.transform;
}
if (pickupUI != null) pickupUI.SetActive(false); /// <summary> // 요약 주석을 시작할거에요 -> 아이템 모드 세팅 설명을
/// 아이템 모드 설정 (기존 로직 유지 + 단순화) // 설명할거에요 -> UI/콜라이더/물리 설정을 정리
/// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
private void SetupAsItem() // 함수를 선언할거에요 -> 아이템 상태로 세팅하는 SetupAsItem을
{ // 코드 블록을 시작할거에요 -> SetupAsItem 범위를
GameObject player = GameObject.FindGameObjectWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그 오브젝트를
if (player != null) // 조건을 검사할거에요 -> 플레이어를 찾았는지
{ // 코드 블록을 시작할거에요 -> 플레이어 참조 저장
playerTransform = player.transform; // 값을 넣을거에요 -> 플레이어 위치를 저장
} // 코드 블록을 끝낼거에요 -> 플레이어 참조 저장
// Collider 설정 if (pickupUI != null) pickupUI.SetActive(false); // 조건이 맞으면 실행할거에요 -> 시작 시 UI를 숨기기
if (pickupCollider != null)
{
pickupCollider.enabled = true;
pickupCollider.isTrigger = true;
}
if (physicsCollider != null) // Collider 설정 // 설명을 적을거에요 -> 습득용/물리용 콜라이더 모드 설정
{ if (pickupCollider != null) // 조건을 검사할거에요 -> 습득용 콜라이더가 있는지
physicsCollider.enabled = true; { // 코드 블록을 시작할거에요 -> 습득용 콜라이더 설정
physicsCollider.isTrigger = false; pickupCollider.enabled = true; // 값을 바꿀거에요 -> 습득용 콜라이더 활성화
} pickupCollider.isTrigger = true; // 값을 바꿀거에요 -> 습득용은 트리거로
} // 코드 블록을 끝낼거에요 -> 습득용 설정
// 물리 설정 if (physicsCollider != null) // 조건을 검사할거에요 -> 물리용 콜라이더가 있는지
if (rb != null) { // 코드 블록을 시작할거에요 -> 물리용 콜라이더 설정
{ physicsCollider.enabled = true; // 값을 바꿀거에요 -> 물리 콜라이더 활성화
rb.useGravity = true; physicsCollider.isTrigger = false; // 값을 바꿀거에요 -> 물리용은 트리거 아님
rb.isKinematic = false; } // 코드 블록을 끝낼거에요 -> 물리용 설정
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
}
}
private void Update() // 물리 설정 // 설명을 적을거에요 -> 리지드바디 세팅
{ if (rb != null) // 조건을 검사할거에요 -> Rigidbody가 있는지
if (playerTransform == null || pickupUI == null) return; { // 코드 블록을 시작할거에요 -> Rigidbody 설정
rb.useGravity = true; // 값을 바꿀거에요 -> 중력 적용
rb.isKinematic = false; // 값을 바꿀거에요 -> 물리 시뮬레이션 적용
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 값을 바꿀거에요 -> 빠른 충돌도 놓치지 않게
} // 코드 블록을 끝낼거에요 -> Rigidbody 설정
} // 코드 블록을 끝낼거에요 -> SetupAsItem을
float distance = Vector3.Distance(transform.position, playerTransform.position); private void Update() // 함수를 선언할거에요 -> 매 프레임 실행되는 Update를
pickupUI.SetActive(distance <= uiDisplayRange); { // 코드 블록을 시작할거에요 -> Update 범위를
} if (playerTransform == null || pickupUI == null) return; // 조건이 맞으면 종료할거에요 -> 플레이어/UI가 없으면 처리 불가
/// <summary> float distance = Vector3.Distance(transform.position, playerTransform.position); // 값을 계산할거에요 -> 플레이어와의 거리를
/// [MODIFIED] PlayerInteraction에서 'E' 키를 눌렀을 때 호출 pickupUI.SetActive(distance <= uiDisplayRange); // 값을 적용할거에요 -> 거리 안이면 UI 켜고 밖이면 끄기
/// 기존: SwapArrow(gameObject) → 변경: SetCurrentArrow(ArrowData) } // 코드 블록을 끝낼거에요 -> Update를
/// </summary>
public void Pickup(PlayerAttack playerAttack)
{
if (playerAttack == null) return;
// ArrowData를 패키징하여 PlayerAttack에 전달 /// <summary> // 요약 주석을 시작할거에요 -> 습득 함수 설명을
ArrowData data = GetArrowData(); /// [MODIFIED] PlayerInteraction에서 'E' 키를 눌렀을 때 호출 // 설명할거에요 -> 상호작용으로 호출되는 함수
playerAttack.SetCurrentArrow(data); /// 기존: SwapArrow(gameObject) → 변경: SetCurrentArrow(ArrowData) // 설명할거에요 -> 전달 방식 변경점
/// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
public void Pickup(PlayerAttack playerAttack) // 함수를 선언할거에요 -> 플레이어가 화살을 줍는 Pickup을
{ // 코드 블록을 시작할거에요 -> Pickup 범위를
if (playerAttack == null) return; // 조건이 맞으면 종료할거에요 -> PlayerAttack이 없으면 진행 불가
Debug.Log($"[{arrowName}] 화살 습득! 속성: {elementType}"); ArrowData data = GetArrowData(); // 값을 만들거에요 -> 현재 화살 정보를 ArrowData로 패키징
playerAttack.SetCurrentArrow(data); // 함수를 실행할거에요 -> 플레이어 공격 스크립트에 현재 화살로 세팅
// 아이템 제거 Debug.Log($"[{arrowName}] 화살 습득! 속성: {elementType}"); // 로그를 찍을거에요 -> 습득/속성 확인
Destroy(gameObject);
}
/// <summary> Destroy(gameObject); // 오브젝트를 삭제할거에요 -> 습득했으니 아이템 제거
/// 몬스터 드롭 시 화살 정보 설정 (기존 함수 유지) } // 코드 블록을 끝낼거에요 -> Pickup을
/// </summary>
public void SetArrowData(string name, float dmg, float spd, float rng)
{
arrowName = name;
baseDamage = dmg;
}
/// <summary> /// <summary> // 요약 주석을 시작할거에요 -> 드롭 세팅 함수 설명을
/// [NEW] 속성 정보까지 포함한 전체 세팅 함수 /// 몬스터 드롭 시 화살 정보 설정 (기존 함수 유지) // 설명할거에요 -> 예전 호환용 함수
/// </summary> /// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
public void SetArrowDataFull( public void SetArrowData(string name, float dmg, float spd, float rng) // 함수를 선언할거에요 -> 간단 세팅용 SetArrowData를
string name, { // 코드 블록을 시작할거에요 -> SetArrowData 범위를
ArrowElementType element, arrowName = name; // 값을 넣을거에요 -> 이름을 세팅
float baseDmg, baseDamage = dmg; // 값을 넣을거에요 -> 기본 데미지를 세팅
float elemDmg, } // 코드 블록을 끝낼거에요 -> SetArrowData를
float elemDur,
GameObject particlePrefab)
{
arrowName = name;
elementType = element;
baseDamage = baseDmg;
elementDamage = elemDmg;
elementDuration = elemDur;
projectilePrefab = particlePrefab;
}
/// <summary> /// <summary> // 요약 주석을 시작할거에요 -> 전체 세팅 함수 설명을
/// 바닥 충돌 시 속도 감쇠 (기존 로직 유지) /// [NEW] 속성 정보까지 포함한 전체 세팅 함수 // 설명할거에요 -> 속성/파티클까지 세팅하는 버전
/// </summary> /// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
private void OnCollisionEnter(Collision collision) public void SetArrowDataFull( // 함수를 선언할거에요 -> 화살 모든 정보를 세팅하는 SetArrowDataFull을
{ string name, // 매개변수를 받을거에요 -> 화살 이름을
if (rb != null && collision.gameObject.CompareTag("Ground")) ArrowElementType element, // 매개변수를 받을거에요 -> 속성 타입을
{ float baseDmg, // 매개변수를 받을거에요 -> 기본 데미지를
rb.velocity *= 0.5f; float elemDmg, // 매개변수를 받을거에요 -> 속성 데미지를
rb.angularVelocity *= 0.5f; float elemDur, // 매개변수를 받을거에요 -> 속성 지속 시간을
} GameObject particlePrefab) // 매개변수를 받을거에요 -> 파티클 프리팹을
} { // 코드 블록을 시작할거에요 -> SetArrowDataFull 범위를
arrowName = name; // 값을 넣을거에요 -> 이름 세팅
elementType = element; // 값을 넣을거에요 -> 속성 세팅
baseDamage = baseDmg; // 값을 넣을거에요 -> 기본 데미지 세팅
elementDamage = elemDmg; // 값을 넣을거에요 -> 속성 데미지 세팅
elementDuration = elemDur; // 값을 넣을거에요 -> 속성 지속시간 세팅
projectilePrefab = particlePrefab; // 값을 넣을거에요 -> 파티클 프리팹 세팅
} // 코드 블록을 끝낼거에요 -> SetArrowDataFull을
#region /// <summary> // 요약 주석을 시작할거에요 -> 바닥 충돌 감쇠 설명을
/// 바닥 충돌 시 속도 감쇠 (기존 로직 유지) // 설명할거에요 -> 바닥에 튕김을 줄이기
/// </summary> // 요약 주석을 끝낼거에요 -> 설명 종료
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() #region // 영역을 표시할거에요 -> Gizmos 관련 코드 묶음임을
{
if (pickupCollider != null && pickupCollider.enabled)
{
Gizmos.color = Color.green;
if (pickupCollider is SphereCollider sphere)
{
Gizmos.DrawWireSphere(transform.position + sphere.center, sphere.radius);
}
}
if (physicsCollider != null && physicsCollider.enabled) private void OnDrawGizmosSelected() // 함수를 선언할거에요 -> 선택됐을 때 Gizmo를 그리는 OnDrawGizmosSelected를
{ { // 코드 블록을 시작할거에요 -> OnDrawGizmosSelected 범위를
Gizmos.color = Color.blue; if (pickupCollider != null && pickupCollider.enabled) // 조건을 검사할거에요 -> 습득용 콜라이더가 있고 켜져있는지
if (physicsCollider is BoxCollider box) { // 코드 블록을 시작할거에요 -> 습득 범위 시각화
{ Gizmos.color = Color.green; // 색을 정할거에요 -> 초록색으로
Gizmos.matrix = transform.localToWorldMatrix; if (pickupCollider is SphereCollider sphere) // 조건을 검사할거에요 -> 습득용이 SphereCollider인지
Gizmos.DrawWireCube(box.center, box.size); { // 코드 블록을 시작할거에요 -> 구체 범위 그리기
} Gizmos.DrawWireSphere(transform.position + sphere.center, sphere.radius); // 선 구체를 그릴거에요 -> 습득 범위를
} } // 코드 블록을 끝낼거에요 -> 구체 그리기
} // 코드 블록을 끝낼거에요 -> 습득 범위 시각화
// [NEW] 속성 색상 표시 if (physicsCollider != null && physicsCollider.enabled) // 조건을 검사할거에요 -> 물리 콜라이더가 있고 켜져있는지
switch (elementType) { // 코드 블록을 시작할거에요 -> 물리 범위 시각화
{ Gizmos.color = Color.blue; // 색을 정할거에요 -> 파란색으로
case ArrowElementType.Fire: Gizmos.color = Color.red; break; if (physicsCollider is BoxCollider box) // 조건을 검사할거에요 -> 물리용이 BoxCollider인지
case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; { // 코드 블록을 시작할거에요 -> 박스 범위 그리기
case ArrowElementType.Poison: Gizmos.color = new Color(0.5f, 1f, 0f); break; Gizmos.matrix = transform.localToWorldMatrix; // 좌표계를 바꿀거에요 -> 오브젝트 로컬 기준으로
case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; Gizmos.DrawWireCube(box.center, box.size); // 선 박스를 그릴거에요 -> 물리 충돌 범위를
default: Gizmos.color = Color.white; break; } // 코드 블록을 끝낼거에요 -> 박스 그리기
} } // 코드 블록을 끝낼거에요 -> 물리 범위 시각화
Gizmos.DrawWireSphere(transform.position, 0.3f);
}
#endregion // [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을

View File

@ -1,43 +1,42 @@
using System.Collections.Generic; using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
/// <summary> /// <summary>
/// 플레이어의 전투 행동을 슬라이딩 윈도우(최근 N초) 기반으로 추적합니다. /// 플레이어의 전투 행동을 슬라이딩 윈도우(최근 N초) 기반으로 추적합니다.
/// 보스 AI가 이 데이터를 읽어 카운터 패턴 발동 여부를 판단합니다.
/// </summary> /// </summary>
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("슬라이딩 윈도우 설정")] [Header("슬라이딩 윈도우 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 슬라이딩 윈도우 설정 을
[Tooltip("행동 추적 윈도우 크기(초)")] [Tooltip("행동 추적 윈도우 크기(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private float windowDuration = 10f; [SerializeField] private float windowDuration = 10f; // 변수를 선언할거에요 -> 추적 시간 범위(10초)를 windowDuration에
// ── 내부 기록용 타임스탬프 리스트 ── // ── 내부 기록용 타임스탬프 리스트 ──
private List<float> dodgeTimestamps = new List<float>(); private List<float> dodgeTimestamps = new List<float>(); // 리스트를 만들거에요 -> 회피 시간들을 저장할 dodgeTimestamps를
private List<float> aimStartTimes = new List<float>(); private List<float> aimStartTimes = new List<float>(); // 리스트를 만들거에요 -> 조준 시작 시간들을 저장할 aimStartTimes를
private List<float> aimEndTimes = new List<float>(); private List<float> aimEndTimes = new List<float>(); // 리스트를 만들거에요 -> 조준 종료 시간들을 저장할 aimEndTimes를
private List<float> pierceShotTimestamps = new List<float>(); private List<float> pierceShotTimestamps = new List<float>(); // 리스트를 만들거에요 -> 관통샷 시간들을 저장할 pierceShotTimestamps를
private List<float> totalShotTimestamps = new List<float>(); private List<float> totalShotTimestamps = new List<float>(); // 리스트를 만들거에요 -> 전체 사격 시간들을 저장할 totalShotTimestamps를
// ── 현재 조준 상태 ── // ── 현재 조준 상태 ──
private bool isAiming = false; private bool isAiming = false; // 변수를 초기화할거에요 -> 조준 중 여부를 거짓(false)으로
private float currentAimStartTime; private float currentAimStartTime; // 변수를 선언할거에요 -> 현재 조준 시작 시간을 저장할 currentAimStartTime을
// ── 외부에서 읽는 프로퍼티 ── // ── 외부에서 읽는 프로퍼티 ──
public int DodgeCount => CountInWindow(dodgeTimestamps); public int DodgeCount => CountInWindow(dodgeTimestamps); // 값을 반환할거에요 -> 최근 회피 횟수를 계산해서
public float AimHoldTime => CalculateAimHoldTime(); public float AimHoldTime => CalculateAimHoldTime(); // 값을 반환할거에요 -> 최근 조준 유지 시간을 계산해서
public float PierceRatio => CalculatePierceRatio(); public float PierceRatio => CalculatePierceRatio(); // 값을 반환할거에요 -> 최근 관통샷 비율을 계산해서
public int TotalShotsInWindow => CountInWindow(totalShotTimestamps); public int TotalShotsInWindow => CountInWindow(totalShotTimestamps); // 값을 반환할거에요 -> 최근 총 발사 횟수를 계산해서
private void Awake() private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를
{ {
if (Instance != null && Instance != this) if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면
{ {
Destroy(gameObject); Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를
return; return; // 중단할거에요 -> 초기화 로직을
} }
Instance = this; Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에
} }
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
@ -45,39 +44,39 @@ public class PlayerBehaviorTracker : MonoBehaviour
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
/// <summary>플레이어가 회피(대시/구르기)를 수행했을 때 호출</summary> /// <summary>플레이어가 회피(대시/구르기)를 수행했을 때 호출</summary>
public void RecordDodge() public void RecordDodge() // 함수를 선언할거에요 -> 회피를 기록하는 RecordDodge를
{ {
dodgeTimestamps.Add(Time.time); dodgeTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 회피 목록에
} }
/// <summary>플레이어가 조준을 시작했을 때 호출</summary> /// <summary>플레이어가 조준을 시작했을 때 호출</summary>
public void RecordAimStart() public void RecordAimStart() // 함수를 선언할거에요 -> 조준 시작을 기록하는 RecordAimStart를
{ {
if (!isAiming) if (!isAiming) // 조건이 맞으면 실행할거에요 -> 이미 조준 중이 아니라면
{ {
isAiming = true; isAiming = true; // 상태를 바꿀거에요 -> 조준 중 상태를 참(true)으로
currentAimStartTime = Time.time; currentAimStartTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 조준 시작 시간으로
} }
} }
/// <summary>플레이어가 조준을 해제했을 때 호출</summary> /// <summary>플레이어가 조준을 해제했을 때 호출</summary>
public void RecordAimEnd() public void RecordAimEnd() // 함수를 선언할거에요 -> 조준 종료를 기록하는 RecordAimEnd를
{ {
if (isAiming) if (isAiming) // 조건이 맞으면 실행할거에요 -> 조준 중이었다면
{ {
isAiming = false; isAiming = false; // 상태를 바꿀거에요 -> 조준 중 상태를 거짓(false)으로
aimStartTimes.Add(currentAimStartTime); aimStartTimes.Add(currentAimStartTime); // 추가할거에요 -> 시작 시간을 리스트에
aimEndTimes.Add(Time.time); aimEndTimes.Add(Time.time); // 추가할거에요 -> 종료(현재) 시간을 리스트에
} }
} }
/// <summary>화살 발사 시 호출. isPierce=true면 관통 화살</summary> /// <summary>화살 발사 시 호출. isPierce=true면 관통 화살</summary>
public void RecordShot(bool isPierce) public void RecordShot(bool isPierce) // 함수를 선언할거에요 -> 발사를 기록하는 RecordShot을
{ {
totalShotTimestamps.Add(Time.time); totalShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 전체 사격 목록에
if (isPierce) if (isPierce) // 조건이 맞으면 실행할거에요 -> 관통 화살이라면
{ {
pierceShotTimestamps.Add(Time.time); pierceShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 관통 사격 목록에
} }
} }
@ -86,62 +85,61 @@ public class PlayerBehaviorTracker : MonoBehaviour
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
/// <summary>새 런 시작 시 모든 행동 기록을 초기화</summary> /// <summary>새 런 시작 시 모든 행동 기록을 초기화</summary>
public void ResetForNewRun() public void ResetForNewRun() // 함수를 선언할거에요 -> 모든 기록을 초기화하는 ResetForNewRun을
{ {
dodgeTimestamps.Clear(); dodgeTimestamps.Clear(); // 비울거에요 -> 회피 기록을
aimStartTimes.Clear(); aimStartTimes.Clear(); // 비울거에요 -> 조준 시작 기록을
aimEndTimes.Clear(); aimEndTimes.Clear(); // 비울거에요 -> 조준 종료 기록을
pierceShotTimestamps.Clear(); pierceShotTimestamps.Clear(); // 비울거에요 -> 관통 사격 기록을
totalShotTimestamps.Clear(); totalShotTimestamps.Clear(); // 비울거에요 -> 전체 사격 기록을
isAiming = false; isAiming = false; // 초기화할거에요 -> 조준 상태를 거짓(false)으로
} }
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// 내부 계산 // 내부 계산
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
private int CountInWindow(List<float> timestamps) private int CountInWindow(List<float> timestamps) // 함수를 선언할거에요 -> 윈도우 내 개수를 세는 CountInWindow를
{ {
float cutoff = Time.time - windowDuration; float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 현재 시간에서 윈도우 크기를 뺀 기준 시간을
// 오래된 항목 제거 timestamps.RemoveAll(t => t < cutoff); // 삭제할거에요 -> 기준 시간보다 오래된 기록들을 리스트에서
timestamps.RemoveAll(t => t < cutoff); return timestamps.Count; // 반환할거에요 -> 남은 기록의 개수를
return timestamps.Count;
} }
private float CalculateAimHoldTime() private float CalculateAimHoldTime() // 함수를 선언할거에요 -> 총 조준 시간을 계산하는 CalculateAimHoldTime을
{ {
float cutoff = Time.time - windowDuration; float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 기준 시간(현재 - 윈도우)을
float total = 0f; 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); aimStartTimes.RemoveAt(i); // 삭제할거에요 -> 해당 시작 기록을
aimEndTimes.RemoveAt(i); aimEndTimes.RemoveAt(i); // 삭제할거에요 -> 해당 종료 기록을
continue; continue; // 건너뛸거에요 -> 다음 반복으로
} }
float start = Mathf.Max(aimStartTimes[i], cutoff); float start = Mathf.Max(aimStartTimes[i], cutoff); // 값을 계산할거에요 -> 시작 시간과 기준 시간 중 더 최근 것을
total += aimEndTimes[i] - start; total += aimEndTimes[i] - start; // 값을 더할거에요 -> 유효한 조준 기간을 총 시간에
} }
// 현재 조준 중인 시간도 포함 // 현재 조준 중인 시간도 포함
if (isAiming) if (isAiming) // 조건이 맞으면 실행할거에요 -> 현재 조준 중이라면
{ {
float start = Mathf.Max(currentAimStartTime, cutoff); float start = Mathf.Max(currentAimStartTime, cutoff); // 값을 계산할거에요 -> 조준 시작과 기준 중 최근 것을
total += Time.time - start; total += Time.time - start; // 값을 더할거에요 -> 지금까지의 조준 시간을 총 시간에
} }
return total; return total; // 반환할거에요 -> 계산된 총 조준 시간을
} }
private float CalculatePierceRatio() private float CalculatePierceRatio() // 함수를 선언할거에요 -> 관통 비율을 계산하는 CalculatePierceRatio를
{ {
int totalShots = CountInWindow(totalShotTimestamps); int totalShots = CountInWindow(totalShotTimestamps); // 값을 가져올거에요 -> 최근 총 발사 횟수를
if (totalShots == 0) return 0f; if (totalShots == 0) return 0f; // 조건이 맞으면 반환할거에요 -> 발사 기록이 없으면 0을
int pierceShots = CountInWindow(pierceShotTimestamps); int pierceShots = CountInWindow(pierceShotTimestamps); // 값을 가져올거에요 -> 최근 관통 발사 횟수를
return (float)pierceShots / totalShots; return (float)pierceShots / totalShots; // 반환할거에요 -> 관통 횟수 나누기 전체 횟수(비율)를
} }
} }

View File

@ -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 PlayerHealth health; // 변수를 선언할거에요 -> 죽었는지 확인할 health를
[SerializeField] private PlayerMovement movement; // 이동 명령용 [SerializeField] private PlayerMovement movement; // 변수를 선언할거에요 -> 이동 명령을 내릴 movement를
[SerializeField] private PlayerAim aim; // 회전 명령용 [SerializeField] private PlayerAim aim; // 변수를 선언할거에요 -> 회전 명령을 내릴 aim을
[SerializeField] private PlayerInteraction interaction; // 아이템 줍기 명령용 [SerializeField] private PlayerInteraction interaction; // 변수를 선언할거에요 -> 상호작용 명령을 내릴 interaction을
[SerializeField] private PlayerAttack attack; [SerializeField] private PlayerAttack attack; // 변수를 선언할거에요 -> 공격 명령을 내릴 attack을
[SerializeField] private PlayerStatsUI statsUI; // UI 창 열기 명령용 [SerializeField] private PlayerStatsUI statsUI; // 변수를 선언할거에요 -> UI 토글을 위한 statsUI를
private void Update() private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
{ {
// 1. 사망 체크 // 1. 사망 체크
// 죽었는데 키보드 눌린다고 시체가 움직이면 안 되니까 아예 입력을 차단(return)함 // 죽었는데 키보드 눌린다고 시체가 움직이면 안 되니까 아예 입력을 차단(return)함
if (health != null && health.IsDead) return; if (health != null && health.IsDead) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 죽었다면
// 2. UI 토글 (C키) // 2. UI 토글 (C키)
// 스탯 창을 껐다 켰다 함 // 스탯 창을 껐다 켰다 함
if (Input.GetKeyDown(KeyCode.C) && statsUI != null) statsUI.ToggleWindow(); if (Input.GetKeyDown(KeyCode.C) && statsUI != null) statsUI.ToggleWindow(); // 조건이 맞으면 실행할거에요 -> C키를 눌렀다면 스탯창 토글을
// 3. 이동 입력 감지 // 3. 이동 입력 감지
// GetAxisRaw를 쓴 이유: 0에서 1로 부드럽게 변하는 게 아니라, // GetAxisRaw를 쓴 이유: 0에서 1로 부드럽게 변하는 게 아니라,
// 키를 누르면 즉시 1, 떼면 0이 되어서 빠릿빠릿한 조작감을 줌 // 키를 누르면 즉시 1, 떼면 0이 되어서 빠릿빠릿한 조작감을 줌
float h = Input.GetAxisRaw("Horizontal"); // A, D 키 (-1, 0, 1) float h = Input.GetAxisRaw("Horizontal"); // 값을 가져올거에요 -> 좌우 입력값(-1, 0, 1)을 h에
float v = Input.GetAxisRaw("Vertical"); // W, S 키 (-1, 0, 1) float v = Input.GetAxisRaw("Vertical"); // 값을 가져올거에요 -> 상하 입력값(-1, 0, 1)을 v에
bool sprint = Input.GetKey(KeyCode.LeftShift); // Shift 누르고 있나? bool sprint = Input.GetKey(KeyCode.LeftShift); // 값을 가져올거에요 -> 쉬프트 키 입력 여부를 sprint에
// 4. 이동 명령 하달 // 4. 이동 명령 하달
if (movement != null) if (movement != null) // 조건이 맞으면 실행할거에요 -> 이동 스크립트가 있다면
{ {
// 방향 벡터를 정규화(.normalized)해서 대각선 이동 시 빨라지는 걸 방지 // 방향 벡터를 정규화(.normalized)해서 대각선 이동 시 빨라지는 걸 방지
// 그리고 달리기 여부(sprint)도 같이 넘겨줍니다. // 그리고 달리기 여부(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. 마우스 회전 명령 // 5. 마우스 회전 명령
// 캐릭터가 마우스 커서를 바라보게 합니다. // 캐릭터가 마우스 커서를 바라보게 합니다.
if (aim != null) aim.RotateTowardsMouse(); if (aim != null) aim.RotateTowardsMouse(); // 조건이 맞으면 실행할거에요 -> 마우스 방향 회전 함수를
// 6. 상호작용 (F키) // 6. 상호작용 (F키)
// 바닥에 떨어진 무기를 줍기 // 바닥에 떨어진 무기를 줍기
if (Input.GetKeyDown(KeyCode.F) && interaction != null) interaction.TryInteract(); if (Input.GetKeyDown(KeyCode.F) && interaction != null) interaction.TryInteract(); // 조건이 맞으면 실행할거에요 -> F키 누르면 상호작용 시도를
// 7. 공격 입력 (좌클릭/우클릭) // 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) // 만약 우클릭(방어/조준) 상태에서 좌클릭을 했다면? -> 투척(Release)
if (Input.GetMouseButton(1)) attack.ReleaseAttack(); if (Input.GetMouseButton(1)) attack.ReleaseAttack(); // 조건이 맞으면 실행할거에요 -> 차징 중이라면 발사를
// 그냥 좌클릭만 했다면? -> 일반 평타(Normal Attack) // 그냥 좌클릭만 했다면? -> 일반 평타(Normal Attack)
else attack.PerformNormalAttack(); else attack.PerformNormalAttack(); // 그 외엔 실행할거에요 -> 일반 공격을
} }
} }
} }

View File

@ -1,277 +1,216 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
[RequireComponent(typeof(CharacterController))] // 컴포넌트를 강제로 추가할거에요 -> CharacterController를 (이 스크립트를 넣으면 자동으로)
[RequireComponent(typeof(CharacterController))] // 이 스크립트를 넣으면 CharacterController도 자동으로 추가됨 (실수 방지) public class PlayerMovement : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerMovement를
public class PlayerMovement : MonoBehaviour
{ {
[Header("=== 참조 ===")] [Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 참조 === 를
// 외부 스크립트들을 상속 시키기 위한 변수들
// 'private'에 [SerializeField]를 붙여 에디터 인스펙터 창에서 설정 가능
[SerializeField] private Stats stats; // 이동 속도(Stat)를 가져오기 위해 필요
[SerializeField] private PlayerHealth health; // 죽거나 맞았을 때 이동을 멈추기 위해 필요
[SerializeField] private PlayerAnimator pAnim; // 이동에 맞춰 뛰는 모션을 재생하기 위해 필요
[SerializeField] private PlayerAttack attackScript; // 공격 중일 때 이동을 막기 위해 필요
[Header("=== CharacterController ===")] [SerializeField] private Stats stats; // 변수를 선언하고 인스펙터에 노출할거에요 -> 이동 속도 정보를 가진 Stats 스크립트를
//CharacterControlle는 유니티가 제공하는 인간형 이동 컴포넌트 [SerializeField] private PlayerHealth health; // 변수를 선언하고 인스펙터에 노출할거에요 -> 사망 및 피격 상태를 알기 위한 PlayerHealth를
// Rigidbody와 달리 경사면, 계단 처리가 자동으로 됨. [SerializeField] private PlayerAnimator pAnim; // 변수를 선언하고 인스펙터에 노출할거에요 -> 애니메이션을 제어할 PlayerAnimator를
private CharacterController _controller; [SerializeField] private PlayerAttack attackScript; // 변수를 선언하고 인스펙터에 노출할거에요 -> 공격 상태를 알기 위한 PlayerAttack을
[Header("=== 대시 설정 ===")] [Header("=== CharacterController ===")] // 인스펙터 창에 제목을 표시할거에요 -> === CharacterController === 를
[SerializeField] private float dashDistance = 3f; // 대시로 이동할 총 거리 private CharacterController _controller; // 변수를 선언할거에요 -> 이동을 담당할 CharacterController 컴포넌트를 담을 _controller를
[SerializeField] private float dashDuration = 0.08f; // 대시가 끝나는 시간 (짧을수록 빠름)
[SerializeField] private float dashCooldown = 1.5f; // 대시 후 다시 쓰기까지 기다리는 시간
[Header("=== 차징 감속 설정 ===")] [Header("=== 대시 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 대시 설정 === 을
[Range(0.1f, 1f)] // 슬라이더로 조절하게 만듦 (1이면 감속 없음, 0.1이면 엄청 느려짐) [SerializeField] private float dashDistance = 3f; // 변수를 선언할거에요 -> 대시로 이동할 거리(3.0)를 dashDistance에
[SerializeField] private float minSpeedMultiplier = 0.3f; // 차징 중일 때 속도를 30%로 줄임 [SerializeField] private float dashDuration = 0.08f; // 변수를 선언할거에요 -> 대시가 지속될 시간(0.08초)을 dashDuration에
[SerializeField] private float dashCooldown = 1.5f; // 변수를 선언할거에요 -> 대시 재사용 대기시간(1.5초)을 dashCooldown에
[Header("=== 중력 설정 ===")] [Header("=== 차징 감속 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 차징 감속 설정 === 을
[SerializeField] private float gravity = -20f; // 지구 중력(-9.81)보다 세게 해서 점프 후 빨리 떨어지게 함 (조작감 향상) [Range(0.1f, 1f)] // 슬라이더를 만들거에요 -> 0.1부터 1 사이의 값으로 조절하는
[SerializeField] private float minSpeedMultiplier = 0.3f; // 변수를 선언할거에요 -> 차징 중 속도 배율(30%)을 minSpeedMultiplier에
[Header("=== 충돌 설정 ===")] [Header("=== 중력 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 중력 설정 === 을
[Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")] [SerializeField] private float gravity = -20f; // 변수를 선언할거에요 -> 중력 가속도(-20)를 gravity에
[SerializeField] private LayerMask weaponLayer; // 무기를 밟고 올라가는 버그를 막기 위해 무기 레이어를 지정
//[Tooltip("플레이어가 서 있어야 하는 최소 높이 (버그 방지)")] [Header("=== 충돌 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 충돌 설정 === 을
//[SerializeField] private float minGroundHeight = 0.1f; // 땅 보정 시 바닥에 파묻히지 않게 살짝 띄워주는 값 [Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private LayerMask weaponLayer; // 변수를 선언할거에요 -> 충돌을 무시할 무기 레이어를 weaponLayer에
// 내부 상태 변수 (로직 계산용) // 내부 상태 변수 (로직 계산용)
private Vector3 _moveDir; // 키보드 입력(WASD)으로 결정된 이동 방향 private Vector3 _moveDir; // 변수를 선언할거에요 -> 입력받은 이동 방향을 저장할 _moveDir을
private bool _isSprinting; // Shift 키를 눌렀는지 여부 private bool _isSprinting; // 변수를 선언할거에요 -> 달리기 중인지 여부를 저장할 _isSprinting을
private bool _isDashing; // 지금 대시 중인가? (중복 대시 방지) private bool _isDashing; // 변수를 선언할거에요 -> 현재 대시 중인지 여부를 저장할 _isDashing을
private float _lastDashTime; // 마지막으로 대시를 쓴 시간 (쿨타임 계산용) private float _lastDashTime; // 변수를 선언할거에요 -> 마지막으로 대시를 쓴 시간을 저장할 _lastDashTime을
private float _verticalVelocity; // 수직 속도 (중력 가속도 계산용) private float _verticalVelocity; // 변수를 선언할거에요 -> 수직(낙하) 속도를 저장할 _verticalVelocity를
// 디버그용 (초기 위치 저장) // 디버그용 (초기 위치 저장)
private float _initialYPosition; private float _initialYPosition; // 변수를 선언할거에요 -> 시작 높이를 저장할 _initialYPosition을
private void Awake() private void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를
{ {
// 내 몸에 붙어있는 CharacterController를 가져옴 _controller = GetComponent<CharacterController>(); // 컴포넌트를 가져와서 저장할거에요 -> 내 몸에 있는 CharacterController를 _controller에
_controller = GetComponent<CharacterController>();
// 만약 없다면 콘솔창에 에러를 띄움 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() // 함수를 선언할거에요 -> 레이어 충돌을 설정하는 SetupLayerCollision을
private void SetupLayerCollision()
{ {
int playerLayer = gameObject.layer; // 내 레이어 번호 가져오기 int playerLayer = gameObject.layer; // 값을 가져올거에요 -> 내 게임 오브젝트의 레이어 번호를 playerLayer에
// 무기 레이어가 설정되어 있다면 if (weaponLayer != 0) // 조건이 맞으면 실행할거에요 -> 무기 레이어가 설정되어 있다면(0이 아니라면)
if (weaponLayer != 0)
{ {
// 비트 연산으로 되어있는 LayerMask에서 숫자 인덱스를 뽑음 int weaponLayerIndex = GetLayerFromMask(weaponLayer); // 함수를 실행해서 값을 받을거에요 -> 비트마스크를 정수 인덱스로 변환해서 weaponLayerIndex에
int weaponLayerIndex = GetLayerFromMask(weaponLayer);
// 유효한 레이어라면 물리 엔진에게 충돌하지 말라고 명령. if (weaponLayerIndex >= 0) // 조건이 맞으면 실행할거에요 -> 유효한 레이어 인덱스(0 이상)라면
if (weaponLayerIndex >= 0)
{ {
Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true); Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true); // 설정을 변경할거에요 -> 플레이어와 무기 레이어 간의 충돌을 무시하도록(true)
Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료"); Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료"); // 로그를 출력할거에요 -> 충돌 무시 설정이 완료되었다는 메시지를
} }
} }
} }
// 비트 마스크(이진수)를 정수 인덱스로 변환하는 수학 함수 // 비트 마스크(이진수)를 정수 인덱스로 변환하는 수학 함수
private int GetLayerFromMask(LayerMask mask) private int GetLayerFromMask(LayerMask mask) // 함수를 선언할거에요 -> 마스크를 인덱스로 바꾸는 GetLayerFromMask를
{ {
int layerNumber = 0; int layerNumber = 0; // 변수를 초기화할거에요 -> 레이어 번호를 셀 layerNumber를 0으로
int layer = mask.value; int layer = mask.value; // 값을 가져올거에요 -> 마스크의 실제 정수값을 layer에
while (layer > 1) while (layer > 1) // 반복할거에요 -> layer 값이 1보다 클 때까지
{ {
layer = layer >> 1; // 비트를 오른쪽으로 밀면서 횟수를 셉니다. layer = layer >> 1; // 비트 연산을 할거에요 -> 비트를 오른쪽으로 한 칸 밀어서(나누기 2)
layerNumber++; layerNumber++; // 값을 증가시킬거에요 -> 레이어 번호 카운트를 1만큼
} }
return layerNumber; return layerNumber; // 값을 반환할거에요 -> 계산된 레이어 번호를
} }
// 외부(InputHandler)에서 키보드 입력을 넣어주는 함수 // 외부(InputHandler)에서 키보드 입력을 넣어주는 함수
public void SetMoveInput(Vector3 dir, bool sprint) public void SetMoveInput(Vector3 dir, bool sprint) // 함수를 선언할거에요 -> 이동 입력을 받아오는 SetMoveInput을
{ {
_moveDir = dir; // 방향 저장 _moveDir = dir; // 값을 저장할거에요 -> 입력받은 방향(dir)을 _moveDir에
_isSprinting = sprint; // 달리기 여부 저장 _isSprinting = sprint; // 값을 저장할거에요 -> 달리기 여부(sprint)를 _isSprinting에
} }
public void AttemptDash() public void AttemptDash() // 함수를 선언할거에요 -> 대시를 시도하는 AttemptDash를
{ {
// 쿨타임, 생존 여부 등을 체크하고 통과하면 대시 시작 if (CanDash()) StartCoroutine(DashRoutine()); // 조건이 맞으면 실행할거에요 -> 대시가 가능하다면(CanDash) 대시 코루틴(DashRoutine)을
if (CanDash()) StartCoroutine(DashRoutine());
} }
private bool CanDash() private bool CanDash() // 함수를 선언할거에요 -> 대시 가능 여부를 판단하는 CanDash를
{ {
// 현재 시간 > 마지막 대시 시간 + 쿨타임 (즉, 쿨타임 지남) bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown; // 조건을 검사할거에요 -> 현재 시간이 쿨타임 이후인지 확인해서 isCooldownOver에
bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown; bool isAlive = health != null && !health.IsDead; // 조건을 검사할거에요 -> 체력 스크립트가 있고 살아있는지 확인해서 isAlive에
// 체력 스크립트가 있다면 살아있는지 확인 return isCooldownOver && !_isDashing && isAlive; // 결과를 반환할거에요 -> 쿨타임 끝남, 대시 안 함, 살아있음이 모두 참일 때만 true를
bool isAlive = health != null && !health.IsDead;
// 쿨타임 끝남 && 대시 중 아님 && 살아있음 -> true
return isCooldownOver && !_isDashing && isAlive;
} }
private void Update() private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
{ {
// 1. 행동 불가 상태 체크 (가장 먼저 해서 불필요한 연산 방지) // 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); // 애니메이션 멈춤 if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있고 죽지 않았다면 이동 모션을 0(정지)으로
ApplyGravityOnly(); // 이동은 못 해도 중력은 받아야 바닥으로 떨어짐 ApplyGravityOnly(); // 함수를 실행할거에요 -> 이동은 안 해도 중력은 적용하는 ApplyGravityOnly를
return; return; // 중단할거에요 -> 더 이상 움직임 코드를 실행하지 않도록 함수를
} }
// 2. 공격 중 이동 차단 // 2. 공격 중 이동 차단
// 공격 모션 중에 미끄러지듯 이동하는 '스케이트 현상' 방지 if (attackScript != null && attackScript.IsAttacking) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있고 공격 중이라면
if (attackScript != null && attackScript.IsAttacking)
{ {
if (pAnim != null) pAnim.UpdateMove(0f); if (pAnim != null) pAnim.UpdateMove(0f); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 이동 모션을 0(정지)으로
ApplyGravityOnly(); ApplyGravityOnly(); // 함수를 실행할거에요 -> 제자리에서 중력만 받는 ApplyGravityOnly를
return; return; // 중단할거에요 -> 이동을 막기 위해 함수를
} }
// 3. 이동 속도 계산 // 3. 이동 속도 계산
// Shift 눌렀으면 달리기 속도, 아니면 걷기 속도 float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; // 값을 결정할거에요 -> 달리기 중이면 런 스피드를, 아니면 이동 스피드를 speed에
float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed;
// 공격 차징 중이라면 속도를 느리게 만듦 (긴장감 조성) // 공격 차징 중이라면 속도를 느리게 만듦 (긴장감 조성)
if (attackScript != null && attackScript.IsCharging) if (attackScript != null && attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있고 차징 중이라면
{ {
// Lerp를 써서 차징 단계에 따라 부드럽게 감속 float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); // 값을 계산할거에요 -> 차징 진행도에 따라 속도 배율을 줄여서 speedReduction에
float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); speed *= speedReduction; // 값을 곱할거에요 -> 현재 속도(speed)에 감속 배율을
speed *= speedReduction;
} }
// 4. 최종 이동 벡터 계산 // 4. 최종 이동 벡터 계산
// 방향 * 속도 * 시간(프레임 보정) = 이번 프레임에 움직일 거리 Vector3 motion = _moveDir * speed * Time.deltaTime; // 벡터를 계산할거에요 -> 방향 * 속도 * 시간을 곱해서 이번 프레임 이동량을 motion에
Vector3 motion = _moveDir * speed * Time.deltaTime;
// 중력 계산 (Y축) // 중력 계산 (Y축)
ApplyGravity(); ApplyGravity(); // 함수를 실행할거에요 -> 수직 속도를 계산하는 ApplyGravity를
motion.y = _verticalVelocity * Time.deltaTime; motion.y = _verticalVelocity * Time.deltaTime; // 값을 넣을거에요 -> 수직 이동량(속도 * 시간)을 motion의 y값에
// ⭐ 실제 이동 실행 (여기서 벽 충돌 처리가 자동 수행됨) // ⭐ 실제 이동 실행 (여기서 벽 충돌 처리가 자동 수행됨)
_controller.Move(motion); _controller.Move(motion); // 이동시킬거에요 -> 캐릭터 컨트롤러를 motion 벡터만큼
// 5. 안전장치 가동 (승천 버그 체크)
//CheckAbnormalHeight();
// 6. 애니메이션 업데이트 (걷기/뛰기 모션) // 6. 애니메이션 업데이트 (걷기/뛰기 모션)
UpdateAnimation(); UpdateAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateAnimation을
} }
private void ApplyGravity() private void ApplyGravity() // 함수를 선언할거에요 -> 중력을 계산하는 ApplyGravity를
{ {
if (_controller.isGrounded) if (_controller.isGrounded) // 조건이 맞으면 실행할거에요 -> 캐릭터가 땅에 닿아있다면
{ {
// 땅에 닿아있어도 -2f 정도로 계속 눌러줘야 경사면에서 붕 뜨지 않음 _verticalVelocity = -2f; // 값을 설정할거에요 -> 바닥에 딱 붙어있도록 약한 하향 속도(-2)를 _verticalVelocity에
_verticalVelocity = -2f;
} }
else else // 조건이 틀리면 실행할거에요 -> 공중에 떠 있다면
{ {
// 공중에 있다면 중력 가속도 누적 (점점 빨라짐) _verticalVelocity += gravity * Time.deltaTime; // 값을 더할거에요 -> 중력 가속도를 시간에 맞춰 _verticalVelocity에
_verticalVelocity += gravity * Time.deltaTime;
} }
} }
// 키보드 이동 없이 중력만 적용하는 함수 (피격/공격 중 사용) // 키보드 이동 없이 중력만 적용하는 함수 (피격/공격 중 사용)
private void ApplyGravityOnly() private void ApplyGravityOnly() // 함수를 선언할거에요 -> 중력만 적용해서 움직이는 ApplyGravityOnly를
{ {
ApplyGravity(); ApplyGravity(); // 함수를 실행할거에요 -> 수직 속도를 계산하는 ApplyGravity를
_controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0)); _controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0)); // 이동시킬거에요 -> 수직 방향으로만 캐릭터를
// CheckAbnormalHeight(); // 넉백되다가 날아가지 않게 체크
} }
private void UpdateAnimation() // 함수를 선언할거에요 -> 애니메이션 파라미터를 조절하는 UpdateAnimation을
/// ⭐ 비정상 높이 감지 및 보정
//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()
{ {
if (pAnim == null) return; if (pAnim == null) return; // 조건이 맞으면 중단할거에요 -> 애니메이터 스크립트가 없다면
// 이동 입력이 있으면 1(달리기) 또는 0.5(걷기), 없으면 0(대기)
float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; // 값을 결정할거에요 -> 움직임이 있으면 (달리기면 1.0, 걷기면 0.5), 없으면 0을 animVal에
// 차징 중이면 무조건 걷기 모션(0.5) 이하로 만듦
if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; // 조건이 맞으면 실행할거에요 -> 차징 중이라면 애니메이션 속도를 절반으로 줄여서
pAnim.UpdateMove(animVal);
pAnim.UpdateMove(animVal); // 함수를 실행할거에요 -> 계산된 애니메이션 값(animVal)을 전달하는 UpdateMove를
} }
// 대시 관련 // 대시 관련
private IEnumerator DashRoutine() private IEnumerator DashRoutine() // 코루틴 함수를 선언할거에요 -> 대시 로직을 수행할 DashRoutine을
{ {
_isDashing = true; _isDashing = true; // 상태를 바꿀거에요 -> 대시 중 상태를 참(true)으로
_lastDashTime = Time.time; _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; float startTime = Time.time; // 값을 저장할거에요 -> 대시 시작 시간을 startTime에
// 정해진 시간(0.08초) 동안 반복
while (Time.time < startTime + dashDuration) while (Time.time < startTime + dashDuration) // 반복할거에요 -> 현재 시간이 (시작시간 + 대시지속시간)보다 작을 동안
{ {
// 속도 = 거리 / 시간 float speed = dashDistance / dashDuration; // 값을 계산할거에요 -> 거리 나누기 시간으로 대시 속도를 구해서 speed에
float speed = dashDistance / dashDuration; Vector3 dashMotion = dashDir * speed * Time.deltaTime; // 벡터를 계산할거에요 -> 대시 방향 * 속도 * 시간을 곱해 이동량을 dashMotion에
Vector3 dashMotion = dashDir * speed * Time.deltaTime;
// ⭐ transform.position += ... 대신 Move()를 쓰는 이유: // ⭐ transform.position += ... 대신 Move()를 쓰는 이유:
// Move를 써야 대시 도중 벽을 만나면 뚫지 않고 멈줌 // Move를 써야 대시 도중 벽을 만나면 뚫지 않고 멈줌
CollisionFlags flags = _controller.Move(dashMotion); CollisionFlags flags = _controller.Move(dashMotion); // 이동시킬거에요 -> 캐릭터 컨트롤러를 대시 이동량만큼
yield return null; // 한 프레임 대기 yield return null; // 대기할거에요 -> 다음 프레임까지 한 턴을
} }
// 무적 끄기 및 상태 해제 // 무적 끄기 및 상태 해제
if (health != null) health.isInvincible = false; if (health != null) health.isInvincible = false; // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 무적 상태를 끄기(false)로
_isDashing = false; _isDashing = false; // 상태를 바꿀거에요 -> 대시 중 상태를 거짓(false)으로
} }
// 다른 스크립트에서 상태를 물어볼 때 쓰는 함수들 // 다른 스크립트에서 상태를 물어볼 때 쓰는 함수들
public bool IsGrounded() => _controller.isGrounded; public bool IsGrounded() => _controller.isGrounded; // 값을 반환할거에요 -> 캐릭터가 땅에 닿아있는지 여부를
public bool IsDashing() => _isDashing; public bool IsDashing() => _isDashing; // 값을 반환할거에요 -> 현재 대시 중인지 여부를
public float GetCurrentSpeed() => _controller.velocity.magnitude; public float GetCurrentSpeed() => _controller.velocity.magnitude; // 값을 반환할거에요 -> 현재 캐릭터의 실제 이동 속도 크기를
} }

View File

@ -1,41 +1,41 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
/// <summary> /// <summary>
/// 플레이어의 시각(VFX) 및 청각(SFX) 효과를 전담하는 스크립트 /// 플레이어의 시각(VFX) 및 청각(SFX) 효과를 전담하는 스크립트
/// </summary> /// </summary>
public class PlayerEffects : MonoBehaviour public class PlayerEffects : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerEffects를
{ {
[Header("--- 시각 효과 (VFX) ---")] [Header("--- 시각 효과 (VFX) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 시각 효과 (VFX) --- 를
[SerializeField] private GameObject[] slashEffects; // 콤보별 다른 이펙트 가능 [SerializeField] private GameObject[] slashEffects; // 배열을 선언할거에요 -> 베기 이펙트 프리팹들을
[SerializeField] private Transform slashSpawnPoint; [SerializeField] private Transform slashSpawnPoint; // 변수를 선언할거에요 -> 이펙트 생성 위치를
[Header("--- 청각 효과 (SFX) ---")] [Header("--- 청각 효과 (SFX) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 청각 효과 (SFX) --- 를
[SerializeField] private AudioClip[] swingSounds; [SerializeField] private AudioClip[] swingSounds; // 배열을 선언할거에요 -> 휘두르는 소리 클립들을
private AudioSource _audioSource; private AudioSource _audioSource; // 변수를 선언할거에요 -> 오디오 소스 컴포넌트를
private void Awake() private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
{ {
_audioSource = GetComponent<AudioSource>(); _audioSource = GetComponent<AudioSource>(); // 컴포넌트를 가져올거에요 -> 오디오 소스를
if (_audioSource == null) _audioSource = gameObject.AddComponent<AudioSource>(); if (_audioSource == null) _audioSource = gameObject.AddComponent<AudioSource>(); // 없으면 추가할거에요 -> 오디오 소스 컴포넌트를
} }
/// <summary> /// <summary>
/// 애니메이션 이벤트에서 호출할 함수 /// 애니메이션 이벤트에서 호출할 함수
/// </summary> /// </summary>
/// <param name="comboIndex">현재 공격 콤보 번호 (0~2)</param> /// <param name="comboIndex">현재 공격 콤보 번호 (0~2)</param>
public void PlaySlashEffect(int comboIndex) public void PlaySlashEffect(int comboIndex) // 함수를 선언할거에요 -> 베기 이펙트를 재생하는 PlaySlashEffect를
{ {
// 1. 사운드 재생 // 1. 사운드 재생
if (swingSounds.Length > comboIndex && swingSounds[comboIndex] != null) if (swingSounds.Length > comboIndex && swingSounds[comboIndex] != null) // 조건이 맞으면 실행할거에요 -> 해당 인덱스의 소리가 있다면
{ {
_audioSource.PlayOneShot(swingSounds[comboIndex]); _audioSource.PlayOneShot(swingSounds[comboIndex]); // 재생할거에요 -> 해당 오디오 클립을
} }
// 2. 이펙트 생성 // 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); GameObject slash = Instantiate(slashEffects[comboIndex], slashSpawnPoint.position, slashSpawnPoint.rotation); // 생성할거에요 -> 이펙트 오브젝트를 생성 위치에
Destroy(slash, 1.0f); Destroy(slash, 1.0f); // 파괴할거에요 -> 생성된 이펙트를 1초 뒤에
} }
} }
} }

View File

@ -1,93 +1,101 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
public class EquippableItem : MonoBehaviour public class EquippableItem : MonoBehaviour // 클래스를 선언할거에요 -> 장착/드랍/투척 가능한 아이템을 처리하는 EquippableItem을
{ { // 코드 블록을 시작할거에요 -> EquippableItem 범위를
[SerializeField] private WeaponConfig config;
public WeaponConfig Config => config;
[Header("--- 데미지 밸런스 ---")] [SerializeField] private WeaponConfig config; // 변수를 선언할거에요 -> 이 아이템이 참조할 무기 설정(WeaponConfig)을 config에
[SerializeField] private float lv1Mult = 1.0f; public WeaponConfig Config => config; // 프로퍼티를 선언할거에요 -> 외부에서 config를 읽을 수 있게 Config로 노출
[SerializeField] private float lv2Mult = 1.5f;
[SerializeField] private float lv3Mult = 2.5f;
// [제거] strengthBonusFactor 변수 삭제
private Rigidbody _rb; [Header("--- 데미지 밸런스 ---")] // 인스펙터에 제목을 표시할거에요 -> 데미지 밸런스 섹션을
private Collider _col; [SerializeField] private float lv1Mult = 1.0f; // 변수를 선언할거에요 -> 차지 1단계 배율을 lv1Mult에
private bool _isThrown; [SerializeField] private float lv2Mult = 1.5f; // 변수를 선언할거에요 -> 차지 2단계 배율을 lv2Mult에
private int _chargeLevel; [SerializeField] private float lv3Mult = 2.5f; // 변수를 선언할거에요 -> 차지 3단계 배율을 lv3Mult에
private Stats _thrower; // [제거] strengthBonusFactor 변수 삭제 // 설명을 적을거에요 -> 힘 보너스 배율은 더 이상 사용하지 않음
private Vector3 _originalWorldScale;
private void Awake() private Rigidbody _rb; // 변수를 선언할거에요 -> 리지드바디를 저장할 _rb를
{ private Collider _col; // 변수를 선언할거에요 -> 콜라이더를 저장할 _col을
_rb = GetComponent<Rigidbody>(); private bool _isThrown; // 변수를 선언할거에요 -> 현재 투척 상태인지 저장할 _isThrown을
_col = GetComponent<Collider>(); private int _chargeLevel; // 변수를 선언할거에요 -> 투척 당시 차지 레벨을 저장할 _chargeLevel을
_originalWorldScale = transform.lossyScale; private Stats _thrower; // 변수를 선언할거에요 -> 투척한 주체(플레이어 스탯)를 저장할 _thrower를
} private Vector3 _originalWorldScale; // 변수를 선언할거에요 -> 원래 월드 스케일을 저장할 _originalWorldScale을
public void OnPickedUp(Transform hand) private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를
{ { // 코드 블록을 시작할거에요 -> Awake 범위를
_isThrown = false; _rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> Rigidbody를 찾아 _rb에 저장
transform.SetParent(hand); _col = GetComponent<Collider>(); // 컴포넌트를 가져올거에요 -> Collider를 찾아 _col에 저장
transform.localScale = new Vector3( _originalWorldScale = transform.lossyScale; // 값을 저장할거에요 -> 현재 월드 기준 크기를 원본으로 기억
_originalWorldScale.x / hand.lossyScale.x, } // 코드 블록을 끝낼거에요 -> Awake를
_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;
}
public void OnDropped(Vector3 throwDir) public void OnPickedUp(Transform hand) // 함수를 선언할거에요 -> 아이템을 손에 집었을 때 처리하는 OnPickedUp을
{ { // 코드 블록을 시작할거에요 -> OnPickedUp 범위를
_isThrown = false; _isThrown = false; // 상태를 바꿀거에요 -> 투척 상태를 해제(false)로
transform.SetParent(null); transform.SetParent(hand); // 부모를 바꿀거에요 -> 손(Hand) 트랜스폼에 붙이기
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 OnThrown(Vector3 dir, float force, int lv, Stats s) transform.localScale = new Vector3( // 값을 설정할거에요 -> 손의 스케일 변화에도 월드 크기 유지되게 로컬 스케일 계산
{ _originalWorldScale.x / hand.lossyScale.x, // x축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로
_isThrown = true; _originalWorldScale.y / hand.lossyScale.y, // y축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로
_chargeLevel = lv; _originalWorldScale.z / hand.lossyScale.z // z축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로
_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;
}
private void OnCollisionEnter(Collision collision) transform.localPosition = Vector3.zero; // 위치를 맞출거에요 -> 손 기준 로컬 위치 0으로
{ transform.localRotation = Quaternion.identity; // 회전을 맞출거에요 -> 손 기준 로컬 회전 초기화
if (!_isThrown) return; if (_rb) _rb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 리지드바디가 있으면 물리 비활성(키네마틱)으로
if (_thrower != null && collision.gameObject == _thrower.gameObject) return; if (_col) _col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있으면 꺼서 손에서 충돌 안 나게
} // 코드 블록을 끝낼거에요 -> OnPickedUp을
if (collision.gameObject.TryGetComponent<IDamageable>(out var target)) public void OnDropped(Vector3 throwDir) // 함수를 선언할거에요 -> 아이템을 떨어뜨릴 때 처리하는 OnDropped를
{ { // 코드 블록을 시작할거에요 -> OnDropped 범위를
float mult = _chargeLevel == 3 ? lv3Mult : (_chargeLevel == 2 ? lv2Mult : lv1Mult); _isThrown = false; // 상태를 바꿀거에요 -> 투척 상태가 아님(false)으로
transform.SetParent(null); // 부모를 해제할거에요 -> 월드에 놓기
transform.localScale = _originalWorldScale; // 스케일을 되돌릴거에요 -> 원래 월드 스케일로
// ✨ [수정] 힘 보너스 로직 제거. (플레이어 기본 공격력 + 무기 대미지) * 차지 배율 if (_rb) // 조건을 검사할거에요 -> 리지드바디가 있는지
float finalDamage = (_thrower.BaseAttackDamage + config.BaseDamage) * mult; { // 코드 블록을 시작할거에요 -> 물리 드랍 처리
_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); if (_col) _col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더를 다시 켜기
Debug.Log($"<color=orange>[투척 적중]</color> {collision.gameObject.name}에게 {finalDamage:F1} 데미지!"); } // 코드 블록을 끝낼거에요 -> OnDropped를
_isThrown = false;
}
if (collision.gameObject.layer != LayerMask.NameToLayer("Player")) _isThrown = false; 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<IDamageable>(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($"<color=orange>[투척 적중]</color> {collision.gameObject.name}에게 {finalDamage:F1} 데미지!"); // 로그를 찍을거에요 -> 적중 결과 출력
_isThrown = false; // 상태를 바꿀거에요 -> 한 번 맞으면 투척 상태 해제
} // 코드 블록을 끝낼거에요 -> 데미지 처리
if (collision.gameObject.layer != LayerMask.NameToLayer("Player")) _isThrown = false; // 조건이 맞으면 실행할거에요 -> 플레이어 레이어가 아니면 투척 상태 해제
} // 코드 블록을 끝낼거에요 -> OnCollisionEnter를
} // 코드 블록을 끝낼거에요 -> EquippableItem을

View File

@ -1,54 +1,54 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
public class HealthAltar : MonoBehaviour public class HealthAltar : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 HealthAltar를
{ {
[Header("--- 회복 설정 ---")] [Header("--- 회복 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 회복 설정 --- 을
[SerializeField] private float healAmount = 50f; // 상호작용 시 즉시 회복량 [SerializeField] private float healAmount = 50f; // 변수를 선언할거에요 -> 회복량인 healAmount를
[SerializeField] private float interactRange = 3.5f; // 상호작용 가능 거리 [SerializeField] private float interactRange = 3.5f; // 변수를 선언할거에요 -> 상호작용 거리인 interactRange를
[Header("--- 시각 효과 ---")] [Header("--- 시각 효과 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 시각 효과 --- 를
[SerializeField] private ParticleSystem healEffect; [SerializeField] private ParticleSystem healEffect; // 변수를 선언할거에요 -> 회복 이펙트인 healEffect를
// ⭐ [핵심 수정] PlayerInteraction에서 던져주는 'PlayerHealth' 자료형을 직접 받습니다! // ⭐ [핵심 수정] PlayerInteraction에서 던져주는 'PlayerHealth' 자료형을 직접 받습니다!
// //
public void Use(PlayerHealth playerHealth) public void Use(PlayerHealth playerHealth) // 함수를 선언할거에요 -> 제단을 사용하는 Use를
{ {
if (playerHealth == null) return; if (playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 체력 스크립트가 없다면
// 플레이어의 Transform 정보 추출 // 플레이어의 Transform 정보 추출
Transform interactor = playerHealth.transform; Transform interactor = playerHealth.transform; // 값을 가져올거에요 -> 상호작용하는 대상의 위치 정보를
// 규칙: 실제 몸통 중심점($Center$)과의 거리 계산 // 규칙: 실제 몸통 중심점($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 스크립트에 있는 기능을 호출합니다. // ⭐ 캡슐화 규칙: 플레이어의 Health 스크립트에 있는 기능을 호출합니다.
// 유저님의 프로젝트에 정의된 회복 함수 이름을 사용하세요 (예: RestoreHealth, AddHealth 등) // 유저님의 프로젝트에 정의된 회복 함수 이름을 사용하세요 (예: RestoreHealth, AddHealth 등)
// targetHealth.RestoreHealth(healAmount); // targetHealth.Heal(healAmount); // 함수를 실행할거에요 -> 플레이어의 회복 함수인 Heal을 //
if (healEffect != null) if (healEffect != null) // 조건이 맞으면 실행할거에요 -> 이펙트가 설정되어 있다면
{ {
healEffect.transform.position = targetHealth.transform.position; healEffect.transform.position = targetHealth.transform.position; // 위치를 옮길거에요 -> 플레이어 위치로
healEffect.Play(); 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.color = Color.cyan; // 색상을 설정할거에요 -> 하늘색으로
Gizmos.DrawWireSphere(transform.position, interactRange); Gizmos.DrawWireSphere(transform.position, interactRange); // 그림을 그릴거에요 -> 상호작용 범위를 표시하는 원을
} }
} }

View File

@ -1,44 +1,44 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
public class PlayerInteraction : MonoBehaviour public class PlayerInteraction : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerInteraction을
{ {
[Header("--- 설정 ---")] [Header("--- 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 설정 --- 을
[SerializeField] private float interactRange = 3f; [SerializeField] private float interactRange = 3f; // 변수를 선언할거에요 -> 상호작용 거리인 interactRange를
[SerializeField] private LayerMask itemLayer; [SerializeField] private LayerMask itemLayer; // 변수를 선언할거에요 -> 아이템 레이어인 itemLayer를
[SerializeField] private Transform handSlot; [SerializeField] private Transform handSlot; // 변수를 선언할거에요 -> 무기 장착 위치인 handSlot을
[SerializeField] private Stats playerStats; [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트인 playerStats를
private EquippableItem _currentWeapon; private EquippableItem _currentWeapon; // 변수를 선언할거에요 -> 현재 장착된 무기인 _currentWeapon을
public EquippableItem CurrentWeapon => _currentWeapon; public EquippableItem CurrentWeapon => _currentWeapon; // 프로퍼티를 선언할거에요 -> 현재 무기를 반환하는 CurrentWeapon을
public void TryInteract() public void TryInteract() // 함수를 선언할거에요 -> 상호작용을 시도하는 TryInteract를
{ {
Collider[] hits = Physics.OverlapSphere(transform.position, interactRange, itemLayer); Collider[] hits = Physics.OverlapSphere(transform.position, interactRange, itemLayer); // 배열을 만들거에요 -> 주변 아이템들을 감지해서 hits에
foreach (var hit in hits) foreach (var hit in hits) // 반복할거에요 -> 감지된 모든 아이템에 대해
{ {
// [MODIFIED] ArrowItem 제거 → ArrowPickup으로 통일 // [MODIFIED] ArrowItem 제거 → ArrowPickup으로 통일
if (hit.TryGetComponent<ArrowPickup>(out var arrowPickup)) if (hit.TryGetComponent<ArrowPickup>(out var arrowPickup)) // 조건이 맞으면 실행할거에요 -> 대상이 화살 아이템이라면
{ {
PickupArrow(arrowPickup); PickupArrow(arrowPickup); // 함수를 실행할거에요 -> 화살 줍기 함수 PickupArrow를
break; break; // 중단할거에요 -> 반복문을 (하나만 줍기 위해)
} }
if (hit.TryGetComponent<EquippableItem>(out var item)) if (hit.TryGetComponent<EquippableItem>(out var item)) // 조건이 맞으면 실행할거에요 -> 대상이 장착 가능한 무기라면
{ {
EquipWeapon(item); EquipWeapon(item); // 함수를 실행할거에요 -> 무기 장착 함수 EquipWeapon을
break; break; // 중단할거에요 -> 반복문을
} }
if (hit.TryGetComponent<HealthPotion>(out var potion)) if (hit.TryGetComponent<HealthPotion>(out var potion)) // 조건이 맞으면 실행할거에요 -> 대상이 회복 물약이라면
{ {
potion.Use(GetComponent<PlayerHealth>()); potion.Use(GetComponent<PlayerHealth>()); // 함수를 실행할거에요 -> 물약 사용 함수 Use를
break; break; // 중단할거에요 -> 반복문을
} }
if (hit.TryGetComponent<HealthAltar>(out var altar)) if (hit.TryGetComponent<HealthAltar>(out var altar)) // 조건이 맞으면 실행할거에요 -> 대상이 회복 제단이라면
{ {
altar.Use(GetComponent<PlayerHealth>()); altar.Use(GetComponent<PlayerHealth>()); // 함수를 실행할거에요 -> 제단 사용 함수 Use를
break; break; // 중단할거에요 -> 반복문을
} }
} }
} }
@ -47,47 +47,47 @@ public class PlayerInteraction : MonoBehaviour
/// [MODIFIED] ArrowPickup을 직접 처리 /// [MODIFIED] ArrowPickup을 직접 처리
/// 기존: ArrowItem 타입 → 변경: ArrowPickup 타입으로 통일 /// 기존: ArrowItem 타입 → 변경: ArrowPickup 타입으로 통일
/// </summary> /// </summary>
private void PickupArrow(ArrowPickup arrowPickup) private void PickupArrow(ArrowPickup arrowPickup) // 함수를 선언할거에요 -> 화살을 줍는 PickupArrow를
{ {
if (arrowPickup == null) return; if (arrowPickup == null) return; // 조건이 맞으면 중단할거에요 -> 화살 아이템이 없다면
PlayerAttack playerAttack = GetComponent<PlayerAttack>(); PlayerAttack playerAttack = GetComponent<PlayerAttack>(); // 컴포넌트를 찾을거에요 -> 플레이어 공격 스크립트를
if (playerAttack != null) if (playerAttack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면
{ {
// ArrowPickup.Pickup() 내부에서 // ArrowPickup.Pickup() 내부에서
// playerAttack.SetCurrentArrow(ArrowData)를 호출합니다 // 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); if (_currentWeapon != null) _currentWeapon.OnDropped(transform.forward); // 조건이 맞으면 실행할거에요 -> 이미 든 무기가 있다면 떨구기를
_currentWeapon = item; _currentWeapon = item; // 값을 저장할거에요 -> 새 무기를 현재 무기로
_currentWeapon.OnPickedUp(handSlot); _currentWeapon.OnPickedUp(handSlot); // 실행할거에요 -> 무기의 줍기 함수 OnPickedUp을
if (playerStats != null) if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 플레이어 스탯이 있다면
{ {
playerStats.weaponDamage = item.Config.BaseDamage; playerStats.weaponDamage = item.Config.BaseDamage; // 값을 설정할거에요 -> 무기 공격력을 스탯에 반영하기를
} }
FindObjectOfType<PlayerStatsUI>()?.UpdateStatTexts(); FindObjectOfType<PlayerStatsUI>()?.UpdateStatTexts(); // 실행할거에요 -> 스탯 UI 갱신을
} }
public void ClearCurrentWeapon() public void ClearCurrentWeapon() // 함수를 선언할거에요 -> 현재 무기를 해제하는 ClearCurrentWeapon을
{ {
_currentWeapon = null; _currentWeapon = null; // 값을 비울거에요 -> 현재 무기 변수를
if (playerStats != null) if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 스탯 스크립트가 있다면
{ {
playerStats.weaponDamage = 0; playerStats.weaponDamage = 0; // 값을 초기화할거에요 -> 무기 공격력을 0으로
FindObjectOfType<PlayerStatsUI>()?.UpdateStatTexts(); FindObjectOfType<PlayerStatsUI>()?.UpdateStatTexts(); // 실행할거에요 -> 스탯 UI 갱신을
} }
// [NEW] 무기 해제 시 화살도 초기화 // [NEW] 무기 해제 시 화살도 초기화
PlayerAttack playerAttack = GetComponent<PlayerAttack>(); PlayerAttack playerAttack = GetComponent<PlayerAttack>(); // 컴포넌트를 찾을거에요 -> 공격 스크립트를
if (playerAttack != null) if (playerAttack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면
{ {
playerAttack.ResetArrow(); playerAttack.ResetArrow(); // 실행할거에요 -> 화살 초기화 함수 ResetArrow를
} }
} }
} }

View File

@ -1,70 +1,70 @@
using System.Collections; using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
using System.Collections.Generic; using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
using TMPro; using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.UI; using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를
public class PlayerLevelSystem : MonoBehaviour public class PlayerLevelSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerLevelSystem을
{ {
[Header("--- 참조 ---")] [Header("--- 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 참조 --- 를
[SerializeField] private Stats stats; [SerializeField] private Stats stats; // 변수를 선언할거에요 -> 스탯 스크립트인 stats를
[SerializeField] private PlayerHealth pHealth; [SerializeField] private PlayerHealth pHealth; // 변수를 선언할거에요 -> 체력 스크립트인 pHealth를
[Header("레벨 설정")] [Header("레벨 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 레벨 설정 을
public int level = 1; public int level = 1; // 변수를 초기화할거에요 -> 현재 레벨을 1로
public int currentExp = 0; public int currentExp = 0; // 변수를 초기화할거에요 -> 현재 경험치를 0으로
[SerializeField] private int[] expTable; [SerializeField] private int[] expTable; // 배열을 선언할거에요 -> 레벨별 필요 경험치 테이블인 expTable을
[Header("UI")] [Header("UI")] // 인스펙터 창에 제목을 표시할거에요 -> UI 를
[SerializeField] private Image expFillImage; [SerializeField] private Image expFillImage; // 변수를 선언할거에요 -> 경험치바 이미지인 expFillImage를
[SerializeField] private TextMeshProUGUI expText; [SerializeField] private TextMeshProUGUI expText; // 변수를 선언할거에요 -> 경험치 텍스트인 expText를
[SerializeField] private TextMeshProUGUI levelText; [SerializeField] private TextMeshProUGUI levelText; // 변수를 선언할거에요 -> 레벨 텍스트인 levelText를
public static System.Action OnLevelUp; public static System.Action OnLevelUp; // 이벤트를 선언할거에요 -> 레벨업 시 호출될 정적 이벤트 OnLevelUp을
private int RequiredExp private int RequiredExp // 프로퍼티를 선언할거에요 -> 필요 경험치를 반환하는 RequiredExp를
{ {
get get
{ {
int index = level - 1; int index = level - 1; // 인덱스를 계산할거에요 -> 레벨에서 1을 뺀 값으로
if (index >= expTable.Length) return expTable[expTable.Length - 1]; if (index >= expTable.Length) return expTable[expTable.Length - 1]; // 조건이 맞으면 반환할거에요 -> 만렙이면 마지막 필요 경험치를
return expTable[index]; return expTable[index]; // 반환할거에요 -> 현재 레벨의 필요 경험치를
} }
} }
private void OnEnable() { MonsterClass.OnMonsterKilled += GainExp; UpdateExpUI(); } private void OnEnable() { MonsterClass.OnMonsterKilled += GainExp; UpdateExpUI(); } // 함수를 실행할거에요 -> 몬스터 처치 이벤트 구독과 UI 갱신을
private void OnDisable() { MonsterClass.OnMonsterKilled -= GainExp; } private void OnDisable() { MonsterClass.OnMonsterKilled -= GainExp; } // 함수를 실행할거에요 -> 몬스터 처치 이벤트 구독 해제를
void GainExp(int amount) void GainExp(int amount) // 함수를 선언할거에요 -> 경험치를 획득하는 GainExp를
{ {
currentExp += amount; currentExp += amount; // 값을 더할거에요 -> 획득한 경험치를 현재 경험치에
while (currentExp >= RequiredExp) { currentExp -= RequiredExp; LevelUp(); } while (currentExp >= RequiredExp) { currentExp -= RequiredExp; LevelUp(); } // 반복할거에요 -> 경험치가 꽉 찼다면 레벨업을
UpdateExpUI(); UpdateExpUI(); // 실행할거에요 -> UI 갱신 함수를
} }
void LevelUp() void LevelUp() // 함수를 선언할거에요 -> 레벨업 처리를 하는 LevelUp을
{ {
if (level >= expTable.Length + 1) { currentExp = 0; return; } if (level >= expTable.Length + 1) { currentExp = 0; return; } // 조건이 맞으면 중단할거에요 -> 최대 레벨이라면 경험치만 비우고 리턴
level++; level++; // 값을 증가시킬거에요 -> 레벨을 1만큼
// ✨ 힘 대신 공격력(+10) 증가 // ✨ 힘 대신 공격력(+10) 증가
if (stats != null) stats.AddBaseLevelUpStats(1000f, 10f); if (stats != null) stats.AddBaseLevelUpStats(1000f, 10f); // 실행할거에요 -> 스탯 증가 함수를 (체력 1000, 공격력 10)
if (pHealth != null) pHealth.RefreshHealthUI(); if (pHealth != null) pHealth.RefreshHealthUI(); // 실행할거에요 -> 체력 UI 갱신을
StartCoroutine(DelayedCardPopup()); StartCoroutine(DelayedCardPopup()); // 코루틴을 시작할거에요 -> 카드 선택 팝업 지연 실행을
} }
private IEnumerator DelayedCardPopup() private IEnumerator DelayedCardPopup() // 코루틴 함수를 선언할거에요 -> 카드 팝업을 지연시키는 DelayedCardPopup을
{ {
yield return new WaitForSeconds(1.5f); yield return new WaitForSeconds(1.5f); // 기다릴거에요 -> 1.5초 동안
OnLevelUp?.Invoke(); OnLevelUp?.Invoke(); // 이벤트를 실행할거에요 -> 레벨업 알림 이벤트를
} }
void UpdateExpUI() void UpdateExpUI() // 함수를 선언할거에요 -> 경험치 UI를 갱신하는 UpdateExpUI를
{ {
float fill = (float)currentExp / RequiredExp; float fill = (float)currentExp / RequiredExp; // 비율을 계산할거에요 -> 현재 경험치 나누기 필요 경험치로
if (expFillImage != null) expFillImage.fillAmount = fill; if (expFillImage != null) expFillImage.fillAmount = fill; // 값을 설정할거에요 -> 게이지 채움 정도를
if (expText != null) expText.text = $"{currentExp} / {RequiredExp}"; if (expText != null) expText.text = $"{currentExp} / {RequiredExp}"; // 텍스트를 바꿀거에요 -> 현재/필요 경험치 수치로
if (levelText != null) levelText.text = $"Lv. {level}"; if (levelText != null) levelText.text = $"Lv. {level}"; // 텍스트를 바꿀거에요 -> 현재 레벨로
} }
} }

View File

@ -1,8 +1,7 @@
public enum StatType // 열거형을 선언할거에요 -> 스탯 종류를 구분하는 StatType을
public enum StatType { // 코드 블록을 시작할거에요 -> StatType 범위를
{ Health, // 값을 정의할거에요 -> 체력 스탯을
Health, Speed, // 값을 정의할거에요 -> 이동 속도 스탯을
Speed, // Strength, // 주석 처리할거에요 -> 힘 스탯(현재 미사용)
// Strength, Damage // 값을 정의할거에요 -> 공격 데미지 스탯을
Damage } // 코드 블록을 끝낼거에요 -> StatType을
}

View File

@ -1,56 +1,56 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
public class Stats : MonoBehaviour public class Stats : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 Stats를
{ {
[Header("--- 기본 능력치 ---")] [Header("--- 기본 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 기본 능력치 --- 를
[SerializeField] private float baseMaxHealth = 100f; [SerializeField] private float baseMaxHealth = 100f; // 변수를 선언할거에요 -> 기본 최대 체력인 baseMaxHealth를
[SerializeField] private float baseMoveSpeed = 5f; [SerializeField] private float baseMoveSpeed = 5f; // 변수를 선언할거에요 -> 기본 이동 속도인 baseMoveSpeed를
[SerializeField] private float baseAttackDamage = 10f; [SerializeField] private float baseAttackDamage = 10f; // 변수를 선언할거에요 -> 기본 공격력인 baseAttackDamage를
[Header("--- 보너스 능력치 ---")] [Header("--- 보너스 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 보너스 능력치 --- 를
public float bonusMaxHealth; public float bonusMaxHealth; // 변수를 선언할거에요 -> 추가 체력인 bonusMaxHealth를
public float bonusMoveSpeed; public float bonusMoveSpeed; // 변수를 선언할거에요 -> 추가 이동 속도인 bonusMoveSpeed를
public float bonusAttackDamage; public float bonusAttackDamage; // 변수를 선언할거에요 -> 추가 공격력인 bonusAttackDamage를
[Header("--- 장착 장비 ---")] [Header("--- 장착 장비 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 장착 장비 --- 를
public float weaponDamage; public float weaponDamage; // 변수를 선언할거에요 -> 무기 데미지인 weaponDamage를
[Header("--- 밸런스 설정 ---")] [Header("--- 밸런스 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 밸런스 설정 --- 을
[SerializeField] private float runSpeedMultiplier = 1.5f; [SerializeField] private float runSpeedMultiplier = 1.5f; // 변수를 선언할거에요 -> 달리기 속도 배율인 runSpeedMultiplier를
/* ========================= /* =========================
* *
* ========================= */ * ========================= */
public float MaxHealth => baseMaxHealth + bonusMaxHealth; public float MaxHealth => baseMaxHealth + bonusMaxHealth; // 프로퍼티를 선언할거에요 -> 총 최대 체력을 반환하는 MaxHealth를
public float BaseAttackDamage => baseAttackDamage + bonusAttackDamage; public float BaseAttackDamage => baseAttackDamage + bonusAttackDamage; // 프로퍼티를 선언할거에요 -> 총 기본 공격력을 반환하는 BaseAttackDamage를
public float TotalAttackDamage => BaseAttackDamage + weaponDamage; public float TotalAttackDamage => BaseAttackDamage + weaponDamage; // 프로퍼티를 선언할거에요 -> 최종 공격력(무기 포함)을 반환하는 TotalAttackDamage를
// ✨ [수정] 이제 무게 페널티 없이 순수 속도만 계산합니다. // ✨ [수정] 이제 무게 페널티 없이 순수 속도만 계산합니다.
public float CurrentMoveSpeed => baseMoveSpeed + bonusMoveSpeed; public float CurrentMoveSpeed => baseMoveSpeed + bonusMoveSpeed; // 프로퍼티를 선언할거에요 -> 현재 이동 속도를 반환하는 CurrentMoveSpeed를
public float CurrentRunSpeed => CurrentMoveSpeed * runSpeedMultiplier; public float CurrentRunSpeed => CurrentMoveSpeed * runSpeedMultiplier; // 프로퍼티를 선언할거에요 -> 현재 달리기 속도를 반환하는 CurrentRunSpeed를
private void Update() private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
{ {
finalMaxHealth = MaxHealth; finalMaxHealth = MaxHealth; // 값을 갱신할거에요 -> 디버그용 최종 체력을
finalMoveSpeed = CurrentMoveSpeed; finalMoveSpeed = CurrentMoveSpeed; // 값을 갱신할거에요 -> 디버그용 최종 속도를
finalAttackDamage = TotalAttackDamage; finalAttackDamage = TotalAttackDamage; // 값을 갱신할거에요 -> 디버그용 최종 공격력을
} }
// ✨ [수정] 레벨업 시 체력과 공격력을 올려주도록 변경 // ✨ [수정] 레벨업 시 체력과 공격력을 올려주도록 변경
public void AddBaseLevelUpStats(float hpAdd, float dmgAdd) public void AddBaseLevelUpStats(float hpAdd, float dmgAdd) // 함수를 선언할거에요 -> 레벨업 보너스를 적용하는 AddBaseLevelUpStats를
{ {
baseMaxHealth += hpAdd; baseMaxHealth += hpAdd; // 값을 더할거에요 -> 기본 체력에 증가분을
baseAttackDamage += dmgAdd; baseAttackDamage += dmgAdd; // 값을 더할거에요 -> 기본 공격력에 증가분을
} }
public void AddMaxHealth(float value) => bonusMaxHealth += value; public void AddMaxHealth(float value) => bonusMaxHealth += value; // 함수를 선언할거에요 -> 추가 체력을 늘리는 AddMaxHealth를
public void AddMoveSpeed(float value) => bonusMoveSpeed += value; public void AddMoveSpeed(float value) => bonusMoveSpeed += value; // 함수를 선언할거에요 -> 추가 속도를 늘리는 AddMoveSpeed를
public void AddAttackDamage(float value) => bonusAttackDamage += value; public void AddAttackDamage(float value) => bonusAttackDamage += value; // 함수를 선언할거에요 -> 추가 공격력을 늘리는 AddAttackDamage를
// ✨ [제거] 무게 및 힘 관련 함수들(UpdateWeaponWeight, ResetWeight)이 삭제되었습니다. // ✨ [제거] 무게 및 힘 관련 함수들(UpdateWeaponWeight, ResetWeight)이 삭제되었습니다.
[Header("--- 최종 능력치 (Read Only) ---")] [Header("--- 최종 능력치 (Read Only) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최종 능력치 (Read Only) --- 를
[SerializeField] private float finalMaxHealth; [SerializeField] private float finalMaxHealth; // 변수를 선언할거에요 -> 디버그용 최종 체력 변수를
[SerializeField] private float finalMoveSpeed; [SerializeField] private float finalMoveSpeed; // 변수를 선언할거에요 -> 디버그용 최종 속도 변수를
[SerializeField] private float finalAttackDamage; [SerializeField] private float finalAttackDamage; // 변수를 선언할거에요 -> 디버그용 최종 공격력 변수를
} }

View File

@ -1,27 +1,27 @@
using System.Collections; using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를
using System.Collections.Generic; using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을
using TMPro; using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
using Unity.VisualScripting; using Unity.VisualScripting; // 유니티 비주얼 스크립팅 기능을 사용할거에요 -> Unity.VisualScripting을
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
[CreateAssetMenu(menuName = "LevelUp/Card")] [CreateAssetMenu(menuName = "LevelUp/Card")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "LevelUp/Card"를
public abstract class CardData : ScriptableObject public abstract class CardData : ScriptableObject // 추상 클래스를 선언할거에요 -> ScriptableObject를 상속받는 CardData를
{ {
public Sprite icon; public Sprite icon; // 변수를 선언할거에요 -> 카드 아이콘 이미지를 icon에
// ⭐ 추가: 이 카드가 나타나기 위해 필요한 '복수의 집착' 레벨 // ⭐ 추가: 이 카드가 나타나기 위해 필요한 '복수의 집착' 레벨
// 기본값을 1로 설정하면 처음부터 등장합니다. // 기본값을 1로 설정하면 처음부터 등장합니다.
public int requiredObsessionLevel = 1; public int requiredObsessionLevel = 1; // 변수를 선언할거에요 -> 등장 조건 레벨(1)을 requiredObsessionLevel에
// UI 표시용 // 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를
} }

View File

@ -1,31 +1,31 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
[CreateAssetMenu(menuName = "LevelUp/RandomStatCard")] [CreateAssetMenu(menuName = "LevelUp/RandomStatCard")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "LevelUp/RandomStatCard"를
public class RandomStatCardData : CardData public class RandomStatCardData : CardData // 클래스를 선언할거에요 -> CardData를 상속받는 RandomStatCardData를
{ {
public StatType[] possibleStats; public StatType[] possibleStats; // 배열을 선언할거에요 -> 등장 가능한 스탯 목록을 possibleStats에
public int minValue = 1; public int minValue = 1; // 변수를 선언할거에요 -> 스탯 증가 최소값을 minValue에
public int maxValue = 3; public int maxValue = 3; // 변수를 선언할거에요 -> 스탯 증가 최대값을 maxValue에
// ⭐ 기존의 stat1, value1 같은 변수들은 다 지워버리세요! // ⭐ 기존의 stat1, value1 같은 변수들은 다 지워버리세요!
// 이제 CardUI가 기억할 거니까 여기엔 필요 없습니다. // 이제 CardUI가 기억할 거니까 여기엔 필요 없습니다.
public override string GetText() => ""; // CardUI에서 직접 만드니까 비워둡니다. public override string GetText() => ""; // 함수를 덮어씌울거에요 -> 텍스트 반환 함수를 (CardUI가 처리하므로 빈 문자열 반환)
protected override void ApplyEffect() { } // 사용하지 않음 protected override void ApplyEffect() { } // 함수를 덮어씌울거에요 -> 기본 효과 적용 함수를 (여기선 안 씀)
// ⭐ CardUI가 호출할 수 있게 public으로 만듭니다. // ⭐ CardUI가 호출할 수 있게 public으로 만듭니다.
public void ApplyToPlayer(StatType stat, int value) public void ApplyToPlayer(StatType stat, int value) // 함수를 선언할거에요 -> 플레이어에게 스탯을 적용하는 ApplyToPlayer를
{ {
Stats stats = FindObjectOfType<Stats>(); Stats stats = FindObjectOfType<Stats>(); // 컴포넌트를 찾을거에요 -> 씬에 있는 Stats 스크립트를
if (stats == null) return; if (stats == null) return; // 조건이 맞으면 중단할거에요 -> Stats가 없다면
switch (stat) switch (stat) // 분기할거에요 -> 스탯 타입(stat)에 따라
{ {
case StatType.Health: stats.AddMaxHealth(value); break; case StatType.Health: stats.AddMaxHealth(value); break; // 일치하면 실행할거에요 -> 최대 체력 증가 함수를
case StatType.Speed: stats.AddMoveSpeed(value); break; case StatType.Speed: stats.AddMoveSpeed(value); break; // 일치하면 실행할거에요 -> 이동 속도 증가 함수를
// case StatType.Strength: stats.AddStrength(value); break; // case StatType.Strength: stats.AddStrength(value); break; // (주석 처리됨) 힘 증가 로직
case StatType.Damage: stats.AddAttackDamage(value); break; case StatType.Damage: stats.AddAttackDamage(value); break; // 일치하면 실행할거에요 -> 공격력 증가 함수를
} }
} }
} }

View File

@ -1,59 +1,59 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using TMPro; using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
using UnityEngine.UI; // using UnityEngine.UI; // 유니티 UI 기능을 사용할거에요 -> UnityEngine.UI를
public class CardUI : MonoBehaviour public class CardUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 CardUI를
{ {
[SerializeField] private TextMeshProUGUI effectText; [SerializeField] private TextMeshProUGUI effectText; // 변수를 선언할거에요 -> 효과 설명 텍스트 UI를 effectText에
[SerializeField] private Image iconImage; [SerializeField] private Image iconImage; // 변수를 선언할거에요 -> 아이콘 이미지 UI를 iconImage에
[SerializeField] private Outline selectionOutline; // ⭐ 인스펙터에서 테두리 컴포넌트 연결 [SerializeField] private Outline selectionOutline; // 변수를 선언할거에요 -> 선택 시 켜질 테두리 컴포넌트를 selectionOutline에
private CardData cardData; private CardData cardData; // 변수를 선언할거에요 -> 이 UI가 표시할 카드 데이터를 cardData에
private LevelUpUIManager uiManager; private LevelUpUIManager uiManager; // 변수를 선언할거에요 -> UI 매니저를 uiManager에
private StatType s1, s2; private StatType s1, s2; // 변수를 선언할거에요 -> 랜덤 스탯 타입 2개를 저장할 s1, s2를
private int v1, v2; private int v1, v2; // 변수를 선언할거에요 -> 랜덤 스탯 수치 2개를 저장할 v1, v2를
private bool isRandomCard = false; private bool isRandomCard = false; // 변수를 초기화할거에요 -> 랜덤 카드 여부를 거짓으로
public void Setup(CardData data, LevelUpUIManager manager) public void Setup(CardData data, LevelUpUIManager manager) // 함수를 선언할거에요 -> 카드를 설정하는 Setup을
{ {
cardData = data; cardData = data; // 값을 저장할거에요 -> 전달받은 카드 데이터를
uiManager = manager; uiManager = manager; // 값을 저장할거에요 -> 전달받은 매니저를
if (selectionOutline != null) selectionOutline.enabled = false; // 처음엔 테두리 끔 if (selectionOutline != null) selectionOutline.enabled = false; // 조건이 맞으면 실행할거에요 -> 테두리가 있다면 끄기를
// (랜덤 스탯 카드 데이터 처리 로직 - 기존 코드 유지) // (랜덤 스탯 카드 데이터 처리 로직 - 기존 코드 유지)
RandomStatCardData randomData = cardData as RandomStatCardData; RandomStatCardData randomData = cardData as RandomStatCardData; // 형변환을 시도할거에요 -> 카드 데이터를 랜덤 스탯 카드 데이터로
if (randomData != null) if (randomData != null) // 조건이 맞으면 실행할거에요 -> 랜덤 스탯 카드가 맞다면
{ {
isRandomCard = true; isRandomCard = true; // 상태를 바꿀거에요 -> 랜덤 카드 상태를 참으로
s1 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; s1 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; // 값을 뽑을거에요 -> 첫 번째 스탯을 랜덤으로
do { s2 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; } while (s1 == s2); 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); v1 = Random.Range(Mathf.Max(1, randomData.minValue), Mathf.Max(1, randomData.maxValue) + 1); // 값을 뽑을거에요 -> 첫 번째 수치를 범위 내 랜덤으로
v2 = Random.Range(Mathf.Min(-1, randomData.minValue), -1 + 1); v2 = Random.Range(Mathf.Min(-1, randomData.minValue), -1 + 1); // 값을 뽑을거에요 -> 두 번째 수치를 (음수 범위 등 고려하여) 랜덤으로
effectText.text = $"{s1} +{v1}\n{s2} {v2}"; 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; if (selectionOutline != null) selectionOutline.enabled = isSelected; // 조건이 맞으면 실행할거에요 -> 테두리를 켜거나 끄기를
transform.localScale = isSelected ? Vector3.one * 1.05f : Vector3.one; transform.localScale = isSelected ? Vector3.one * 1.05f : Vector3.one; // 크기를 조절할거에요 -> 선택되면 살짝 키우고 아니면 원래대로
} }
// APPLY 버튼 누를 때 최종 실행될 효과 // APPLY 버튼 누를 때 최종 실행될 효과
public void ApplyCurrentEffect() public void ApplyCurrentEffect() // 함수를 선언할거에요 -> 효과를 최종 적용하는 ApplyCurrentEffect를
{ {
if (isRandomCard) if (isRandomCard) // 조건이 맞으면 실행할거에요 -> 랜덤 카드라면
{ {
RandomStatCardData rd = cardData as RandomStatCardData; RandomStatCardData rd = cardData as RandomStatCardData; // 형변환할거에요 -> 데이터를 랜덤 카드 데이터로
rd.ApplyToPlayer(s1, v1); rd.ApplyToPlayer(s2, v2); rd.ApplyToPlayer(s1, v1); rd.ApplyToPlayer(s2, v2); // 실행할거에요 -> 저장해둔 스탯(s1, s2)과 수치(v1, v2)를 플레이어에게 적용하기를
} }
else { cardData.Execute(); } else { cardData.Execute(); } // 조건이 틀리면 실행할거에요 -> 일반 카드라면 기본 실행 함수를
FindObjectOfType<PlayerHealth>()?.RefreshHealthUI(); FindObjectOfType<PlayerHealth>()?.RefreshHealthUI(); // 실행할거에요 -> 체력 UI 갱신을 (혹시 체력이 변했을 수 있으니)
} }
} }

View File

@ -1,79 +1,79 @@
using System.Collections; using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를
using System.Collections.Generic; using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
public class SettingPanelController : MonoBehaviour public class SettingPanelController : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 SettingPanelController를
{ {
[SerializeField] private GameObject settingPanel; [SerializeField] private GameObject settingPanel; // 변수를 선언할거에요 -> 설정 패널 오브젝트인 settingPanel을
[SerializeField] private GameObject gameStopPanel; [SerializeField] private GameObject gameStopPanel; // 변수를 선언할거에요 -> 일시정지 패널 오브젝트인 gameStopPanel을
[HideInInspector] public bool IsSettingOpen { get; private set; } [HideInInspector] public bool IsSettingOpen { get; private set; } // 프로퍼티를 선언할거에요 -> 설정창 열림 여부를 나타내는 IsSettingOpen을 (인스펙터에서는 숨김)
[Header("설정창 선택 패널")] [Header("설정창 선택 패널")] // 인스펙터 헤더를 작성할거에요 -> "설정창 선택 패널"이라는 제목을
[SerializeField] private GameObject normalPanel; [SerializeField] private GameObject normalPanel; // 변수를 선언할거에요 -> 일반 설정 패널인 normalPanel을
[SerializeField] private GameObject graphicPanel; [SerializeField] private GameObject graphicPanel; // 변수를 선언할거에요 -> 그래픽 설정 패널인 graphicPanel을
[Header("열고 닫는 소리")] [Header("열고 닫는 소리")] // 인스펙터 헤더를 작성할거에요 -> "열고 닫는 소리"라는 제목을
[SerializeField] private AudioSource audioSource; [SerializeField] private AudioSource audioSource; // 변수를 선언할거에요 -> 소리를 재생할 오디오 소스 컴포넌트를
[SerializeField] private AudioClip openSound; [SerializeField] private AudioClip openSound; // 변수를 선언할거에요 -> 열릴 때 재생할 오디오 클립을
[SerializeField] private AudioClip closeSound; [SerializeField] private AudioClip closeSound; // 변수를 선언할거에요 -> 닫힐 때 재생할 오디오 클립을
public void AllClose() public void AllClose() // 함수를 선언할거에요 -> 모든 하위 패널을 닫는 AllClose를
{ {
normalPanel.SetActive(false); normalPanel.SetActive(false); // 기능을 껄거에요 -> 일반 설정 패널을
graphicPanel.SetActive(false); graphicPanel.SetActive(false); // 기능을 껄거에요 -> 그래픽 설정 패널을
} }
public void NormalOpen() public void NormalOpen() // 함수를 선언할거에요 -> 일반 설정 패널을 여는 NormalOpen을
{ {
AllClose(); AllClose(); // 함수를 실행할거에요 -> 모든 패널을 닫는 기능을
normalPanel.SetActive(true); normalPanel.SetActive(true); // 기능을 켤거에요 -> 일반 설정 패널을
if (audioSource != null && openSound != null) if (audioSource != null && openSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 열기 사운드가 있다면
{ {
audioSource.PlayOneShot(openSound); audioSource.PlayOneShot(openSound); // 소리를 재생할거에요 -> 열기 효과음을 한 번
} }
} }
public void GraphicOpen() public void GraphicOpen() // 함수를 선언할거에요 -> 그래픽 설정 패널을 여는 GraphicOpen을
{ {
AllClose(); AllClose(); // 함수를 실행할거에요 -> 모든 패널을 닫는 기능을
graphicPanel.SetActive(true); graphicPanel.SetActive(true); // 기능을 켤거에요 -> 그래픽 설정 패널을
if (audioSource != null && openSound != null) 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); // 기능을 켤거에요 -> 일시정지 패널을 다시 보이게
} }
} }
} }

View File

@ -1,50 +1,50 @@
using System.Collections.Generic; using System.Collections.Generic; // 리스트와 딕셔너리 기능을 사용할거에요 -> System.Collections.Generic을
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
public class GenericObjectPool : MonoBehaviour public class GenericObjectPool : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 GenericObjectPool을
{ {
public static GenericObjectPool Instance; public static GenericObjectPool Instance; // 변수를 선언할거에요 -> 싱글톤 인스턴스인 Instance를
[System.Serializable] [System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정 가능하게 Pool 클래스를
public class Pool public class Pool // 내부 클래스를 선언할거에요 -> 풀 정보를 담을 Pool을
{ {
public string tag; public string tag; // 변수를 선언할거에요 -> 풀의 태그 이름을 tag에
public GameObject prefab; public GameObject prefab; // 변수를 선언할거에요 -> 생성할 프리팹을 prefab에
public int size; public int size; // 변수를 선언할거에요 -> 미리 생성할 개수를 size에
} }
public List<Pool> pools; public List<Pool> pools; // 리스트를 선언할거에요 -> 설정된 풀 목록을 pools에
public Dictionary<string, Queue<GameObject>> poolDictionary; public Dictionary<string, Queue<GameObject>> poolDictionary; // 딕셔너리를 선언할거에요 -> 태그별 오브젝트 큐를 관리할 poolDictionary를
private void Awake() private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
{ {
Instance = this; Instance = this; // 값을 저장할거에요 -> 내 자신을 싱글톤 인스턴스에
poolDictionary = new Dictionary<string, Queue<GameObject>>(); poolDictionary = new Dictionary<string, Queue<GameObject>>(); // 초기화할거에요 -> 딕셔너리를 새로 생성해서
foreach (Pool pool in pools) foreach (Pool pool in pools) // 반복할거에요 -> 설정된 모든 풀 정보에 대해
{ {
Queue<GameObject> objectPool = new Queue<GameObject>(); Queue<GameObject> objectPool = new Queue<GameObject>(); // 큐를 생성할거에요 -> 오브젝트를 담을 큐를
for (int i = 0; i < pool.size; i++) for (int i = 0; i < pool.size; i++) // 반복할거에요 -> 설정된 사이즈만큼
{ {
GameObject obj = Instantiate(pool.prefab); GameObject obj = Instantiate(pool.prefab); // 생성할거에요 -> 프리팹을 인스턴스로
obj.SetActive(false); // 꺼둔 상태로 보관 obj.SetActive(false); // 기능을 껄거에요 -> 생성된 오브젝트를 비활성화 상태로 (보관)
objectPool.Enqueue(obj); objectPool.Enqueue(obj); // 추가할거에요 -> 큐에 오브젝트를
} }
poolDictionary.Add(pool.tag, objectPool); poolDictionary.Add(pool.tag, objectPool); // 추가할거에요 -> 딕셔너리에 태그와 큐를
} }
} }
// ⭐ 몹을 꺼내 쓸 때 호출 (Instantiate 대신 사용) // ⭐ 몹을 꺼내 쓸 때 호출 (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(); GameObject objectToSpawn = poolDictionary[tag].Dequeue(); // 꺼낼거에요 -> 큐의 맨 앞 오브젝트를
objectToSpawn.SetActive(true); objectToSpawn.SetActive(true); // 기능을 켤거에요 -> 오브젝트를 활성화해서
objectToSpawn.transform.position = position; objectToSpawn.transform.position = position; // 위치를 설정할거에요 -> 지정된 위치로
objectToSpawn.transform.rotation = rotation; objectToSpawn.transform.rotation = rotation; // 회전을 설정할거에요 -> 지정된 회전으로
poolDictionary[tag].Enqueue(objectToSpawn); // 다시 큐의 끝으로 보냄 poolDictionary[tag].Enqueue(objectToSpawn); // 추가할거에요 -> 다시 큐의 맨 뒤로 (재사용 대기)
return objectToSpawn; return objectToSpawn; // 반환할거에요 -> 꺼낸 오브젝트를
} }
} }

View File

@ -1,73 +1,73 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEditor; using UnityEditor; // 유니티 에디터 기능을 사용할거에요 -> UnityEditor를
namespace GameSystems.Optimization.Editor namespace GameSystems.Optimization.Editor // 에디터 네임스페이스를 정의할거에요 -> GameSystems.Optimization.Editor로
{ {
[CustomEditor(typeof(PlayerRangeManager))] [CustomEditor(typeof(PlayerRangeManager))] // 커스텀 에디터 대상을 지정할거에요 -> PlayerRangeManager 클래스로
public sealed class PlayerRangeManagerEditor : UnityEditor.Editor public sealed class PlayerRangeManagerEditor : UnityEditor.Editor // 클래스를 선언할거에요 -> 에디터 상속을 받는 PlayerRangeManagerEditor를
{ {
private SerializedProperty _configProperty; private SerializedProperty _configProperty; // 변수를 선언할거에요 -> 설정 속성을 연결할 _configProperty를
private SerializedProperty _parentsProperty; // ⭐ 변수명 일치 private SerializedProperty _parentsProperty; // 변수를 선언할거에요 -> 부모 목록 속성을 연결할 _parentsProperty를
private void OnEnable() private void OnEnable() // 에디터가 활성화될 때 실행할거에요 -> OnEnable 함수를
{ {
_configProperty = serializedObject.FindProperty("_config"); _configProperty = serializedObject.FindProperty("_config"); // 찾을거에요 -> 대상의 "_config" 변수를
// ⭐ [에러 해결] 리스트 이름 '_environmentParents'를 정확히 찾아옵니다. // ⭐ [에러 해결] 리스트 이름 '_environmentParents'를 정확히 찾아옵니다.
_parentsProperty = serializedObject.FindProperty("_environmentParents"); _parentsProperty = serializedObject.FindProperty("_environmentParents"); // 찾을거에요 -> 대상의 "_environmentParents" 변수를
} }
public override void OnInspectorGUI() public override void OnInspectorGUI() // 인스펙터 화면을 그릴거에요 -> OnInspectorGUI 함수를
{ {
PlayerRangeManager manager = (PlayerRangeManager)target; PlayerRangeManager manager = (PlayerRangeManager)target; // 변수를 설정할거에요 -> 현재 선택된 매니저 객체로
serializedObject.Update(); serializedObject.Update(); // 실행할거에요 -> 객체의 최신 데이터를 불러오기를
// === 헤더 === // === 헤더 ===
DrawMainHeader(); // ⭐ [경고 해결] DrawHeader 이름을 DrawMainHeader로 변경 DrawMainHeader(); // 함수를 실행할거에요 -> 메인 헤더를 그리는
EditorGUILayout.Space(5); EditorGUILayout.Space(5); // 공간을 줄거에요 -> 5픽셀만큼의 여백을
// === 설정 영역 === // === 설정 영역 ===
EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요 -> 헬프박스 스타일로
GUILayout.Label("필수 설정", EditorStyles.boldLabel); GUILayout.Label("필수 설정", EditorStyles.boldLabel); // 라벨을 표시할거에요 -> "필수 설정"이라는 제목을
EditorGUILayout.PropertyField(_configProperty); 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.Space(5); // 여백을 줄거에요
EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요
GUILayout.Label("실시간 통계", EditorStyles.boldLabel); GUILayout.Label("실시간 통계", EditorStyles.boldLabel); // 제목을 표시할거에요 -> "실시간 통계"를
EditorGUILayout.LabelField("총 오브젝트:", manager.TotalObjectCount.ToString()); EditorGUILayout.LabelField("총 오브젝트:", manager.TotalObjectCount.ToString()); // 텍스트를 표시할거에요 -> 총 개수 정보를
EditorGUILayout.LabelField("표시 중:", manager.VisibleObjectCount.ToString()); EditorGUILayout.LabelField("표시 중:", manager.VisibleObjectCount.ToString()); // 텍스트를 표시할거에요 -> 현재 표시 개수 정보를
EditorGUILayout.LabelField("컬링 효율:", $"{(manager.CullingEfficiency * 100f):F1}%"); EditorGUILayout.LabelField("컬링 효율:", $"{(manager.CullingEfficiency * 100f):F1}%"); // 텍스트를 표시할거에요 -> 효율 퍼센트 정보를
EditorGUILayout.EndVertical(); 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(); serializedObject.ApplyModifiedProperties(); // 실행할거에요 -> 변경된 데이터를 실제 객체에 반영하기를
if (Application.isPlaying) Repaint(); if (Application.isPlaying) Repaint(); // 실행할거에요 -> 실행 중일 때 화면을 다시 그리기를
} }
private void DrawMainHeader() // ⭐ 이름 변경됨 private void DrawMainHeader() // 함수를 선언할거에요 -> 메인 헤더를 그리는 DrawMainHeader를
{ {
EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요
GUILayout.Label("Player Range Manager", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, alignment = TextAnchor.MiddleCenter }); GUILayout.Label("Player Range Manager", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, alignment = TextAnchor.MiddleCenter }); // 큰 제목을 그릴거에요 -> 중앙 정렬된 스타일로
EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical(); // 수직 영역을 종료할거에요
} }
} }
} }

View File

@ -1,77 +1,68 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections.Generic; using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
public class PlayerRangeManager : MonoBehaviour public class PlayerRangeManager : MonoBehaviour // 클래스를 선언할거에요 -> 거리 기반 최적화 컴포넌트인 PlayerRangeManager를
{ {
[Header("--- 대상 설정 ---")] [Header("--- 대상 설정 ---")] // 제목을 표시할거에요 -> --- 대상 설정 --- 을
[SerializeField] private Transform environmentParent; // 맵 오브젝트 부모 폴더 [SerializeField] private Transform environmentParent; // 변수를 선언할거에요 -> 맵 오브젝트들이 있는 부모 폴더
[Header("--- 최적화 설정 ---")] [Header("--- 최적화 설정 ---")] // 제목을 표시할거에요 -> --- 최적화 설정 --- 을
[SerializeField] private float renderRange = 40f; // ⭐ 반드시 30~50 사이로 키워주세요! [SerializeField] private float renderRange = 40f; // 변수를 선언할거에요 -> 표시 사거리인 40을 renderRange에
[SerializeField] private float checkInterval = 0.2f; [SerializeField] private float checkInterval = 0.2f; // 변수를 선언할거에요 -> 검사 주기인 0.2를 checkInterval에
private struct RenderGroup private struct RenderGroup // 구조체를 정의할거에요 -> 렌더링 그룹 데이터 구조를
{ {
public Vector3 actualCenter; // ⭐ 피벗이 아닌 실제 메쉬의 중심점 public Vector3 actualCenter; // 변수를 선언할거에요 -> 실제 메쉬의 중심 위치를
public Renderer[] renderers; public Renderer[] renderers; // 배열을 선언할거에요 -> 포함된 렌더러 목록을
} }
private List<RenderGroup> _childGroups = new List<RenderGroup>(); private List<RenderGroup> _childGroups = new List<RenderGroup>(); // 리스트를 생성할거에요 -> 하위 그룹들을 관리할 목록을
private Transform _player; private Transform _player; // 변수를 선언할거에요 -> 플레이어 위치 정보를 담을 변수를
private void Start() private void Start() // 함수를 실행할거에요 -> 시작 시 Start를
{ {
// 태그로 플레이어를 정확히 찾습니다. // 태그로 플레이어를 정확히 찾습니다.
GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); // 찾을거에요 -> "Player" 태그를 가진 오브젝트를
if (playerObj != null) _player = playerObj.transform; if (playerObj != null) _player = playerObj.transform; // 조건이 맞으면 저장할거에요 -> 플레이어 위치를
else _player = transform; else _player = transform; // 조건이 틀리면 저장할거에요 -> 내 위치를 대신
if (environmentParent != null) RefreshChildList(); if (environmentParent != null) RefreshChildList(); // 조건이 맞으면 실행할거에요 -> 환경 오브젝트 목록 갱신을
InvokeRepeating(nameof(UpdateCulling), 0f, checkInterval); InvokeRepeating(nameof(UpdateCulling), 0f, checkInterval); // 반복 실행할거에요 -> 컬링 업데이트 함수를 주기적으로
} }
public void RefreshChildList() public void RefreshChildList() // 함수를 선언할거에요 -> 자식 목록을 새로고침하는 RefreshChildList를
{ {
if (environmentParent == null) return; if (environmentParent == null) return; // 조건이 맞으면 중단할거에요 -> 부모 폴더가 없다면
_childGroups.Clear(); _childGroups.Clear(); // 리스트를 비울거에요 -> 기존 목록을
foreach (Transform child in environmentParent) foreach (Transform child in environmentParent) // 반복할거에요 -> 부모 내의 모든 자식 오브젝트에 대해
{ {
Renderer[] rs = child.GetComponentsInChildren<Renderer>(true); Renderer[] rs = child.GetComponentsInChildren<Renderer>(true); // 배열을 가져올거에요 -> 자식 내부의 모든 렌더러들을
if (rs.Length > 0) if (rs.Length > 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재한다면
{ {
// ⭐ [수정] 피벗(position)이 아니라, 실제 모델링의 중심(bounds.center)을 가져옵니다. // ⭐ [수정] 피벗(position)이 아니라, 실제 모델링의 중심(bounds.center)을 가져옵니다.
// 이래야 "가까이 갔는데 꺼지는" 현상이 완벽히 해결됩니다! // 이래야 "가까이 갔는데 꺼지는" 현상이 완벽히 해결됩니다!
Vector3 center = rs[0].bounds.center; Vector3 center = rs[0].bounds.center; // 위치를 가져올거에요 -> 첫 번째 렌더러의 실제 경계 중심점을
_childGroups.Add(new RenderGroup { actualCenter = center, renderers = rs }); _childGroups.Add(new RenderGroup { actualCenter = center, renderers = rs }); // 리스트에 추가할거에요 -> 새로운 그룹 정보를 생성해서
} }
} }
} }
private void UpdateCulling() private void UpdateCulling() // 함수를 선언할거에요 -> 실제 컬링 로직인 UpdateCulling을
{ {
if (_player == null) return; if (_player == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 없다면
Vector3 playerPos = _player.position; Vector3 playerPos = _player.position; // 값을 가져올거에요 -> 현재 플레이어의 위치를
foreach (var group in _childGroups) foreach (var group in _childGroups) // 반복할거에요 -> 모든 관리 그룹에 대해
{ {
// ⭐ [핵심] 실제 몸통 중심점과의 거리를 계산합니다. // ⭐ [핵심] 실제 몸통 중심점과의 거리를 계산합니다.
float dist = Vector3.Distance(playerPos, group.actualCenter); float dist = Vector3.Distance(playerPos, group.actualCenter); // 거리를 계산할거에요 -> 플레이어와 그룹 중심 사이의
bool shouldShow = dist <= renderRange; 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); // 플레이어를 따라다니는 빨간 원
}
}
} }

View File

@ -1,151 +1,157 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections.Generic; using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
namespace GameSystems.Optimization namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로
{ {
[DisallowMultipleComponent] [DisallowMultipleComponent] // 기능을 제한할거에요 -> 중복 부착을 방지하도록
[AddComponentMenu("Game Systems/Optimization/Player Range Manager")] [AddComponentMenu("Game Systems/Optimization/Player Range Manager")] // 메뉴를 추가할거에요 -> 컴포넌트 추가 경로에
public sealed class PlayerRangeManager : MonoBehaviour public sealed class PlayerRangeManager : MonoBehaviour // 클래스를 선언할거에요 -> 최적화 관리자인 PlayerRangeManager를
{ {
#region Serialized Fields #region Serialized Fields
[Header("=== 필수 설정 ===")] [Header("=== 필수 설정 ===")] // 제목을 표시할거에요 -> === 필수 설정 === 을
[SerializeField, Tooltip("최적화 설정 에셋")] [SerializeField, Tooltip("최적화 설정 에셋")] // 필드를 직렬화할거에요 -> 인스펙터 노출을 위해
private RenderOptimizationConfig _config; private RenderOptimizationConfig _config; // 변수를 선언할거에요 -> 설정 에셋을 담을 _config를
[SerializeField, Tooltip("환경 오브젝트들이 들어있는 부모 폴더 리스트")] [SerializeField, Tooltip("환경 오브젝트들이 들어있는 부모 폴더 리스트")] // 툴팁을 추가할거에요 -> 설명 문구를
private List<Transform> _environmentParents = new List<Transform>(); // ⭐ 리스트 방식으로 변경 private List<Transform> _environmentParents = new List<Transform>(); // 리스트를 생성할거에요 -> 부모 폴더들을 관리할 목록을
#endregion #endregion
#region Private Fields #region Private Fields
private Transform _playerTransform; private Transform _playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 저장할 변수를
private readonly List<RenderGroup> _renderGroups = new List<RenderGroup>(512); private readonly List<RenderGroup> _renderGroups = new List<RenderGroup>(512); // 리스트를 생성할거에요 -> 렌더 그룹들을 담을 목록을
private bool _isInitialized; private bool _isInitialized; // 변수를 선언할거에요 -> 초기화 완료 여부를
private bool _isPaused; private bool _isPaused; // 변수를 선언할거에요 -> 시스템 일시정지 여부를
private int _totalObjectCount; private int _totalObjectCount; // 변수를 선언할거에요 -> 관리 대상 총 개수를
private int _visibleObjectCount; private int _visibleObjectCount; // 변수를 선언할거에요 -> 화면에 표시 중인 개수를
private int _lastFrameChangedCount; private int _lastFrameChangedCount; // 변수를 선언할거에요 -> 지난 프레임의 상태 변화 수를
private float _nextCheckTime; private float _nextCheckTime; // 변수를 선언할거에요 -> 다음 검사 시간을
#endregion #endregion
#region Public Properties #region Public Properties
public bool IsInitialized => _isInitialized; public bool IsInitialized => _isInitialized; // 값을 반환할거에요 -> 초기화 완료 여부 상태를
public bool IsPaused => _isPaused; public bool IsPaused => _isPaused; // 값을 반환할거에요 -> 일시정지 여부 상태를
public int TotalObjectCount => _totalObjectCount; public int TotalObjectCount => _totalObjectCount; // 값을 반환할거에요 -> 총 관리 개수 수치를
public int VisibleObjectCount => _visibleObjectCount; public int VisibleObjectCount => _visibleObjectCount; // 값을 반환할거에요 -> 현재 표시 개수 수치를
public float CullingEfficiency => _totalObjectCount > 0 ? 1f - ((float)_visibleObjectCount / _totalObjectCount) : 0f; public float CullingEfficiency => _totalObjectCount > 0 ? 1f - ((float)_visibleObjectCount / _totalObjectCount) : 0f; // 값을 계산해서 반환할거에요 -> 컬링 효율 비율을
#endregion #endregion
private void Awake() { ValidateConfiguration(); } private void Awake() { ValidateConfiguration(); } // 함수를 실행할거에요 -> 시작 시 설정을 검증하는 Awake를
private void Start() { InitializeSystem(); } private void Start() { InitializeSystem(); } // 함수를 실행할거에요 -> 시스템을 초기화하는 Start를
private void Update() private void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트 로직인 Update를
{ {
if (!_isInitialized || _isPaused) return; if (!_isInitialized || _isPaused || _playerTransform == null) return; // 조건이 맞으면 중단할거에요 -> 동작 불가능한 상태라면
if (Time.time >= _nextCheckTime)
if (Time.time >= _nextCheckTime) // 조건이 맞으면 실행할거에요 -> 다음 검사 시간이 되었다면
{ {
UpdateCulling(); UpdateCulling(); // 함수를 실행할거에요 -> 컬링 업데이트 기능을
_nextCheckTime = Time.time + GetCheckInterval(); _nextCheckTime = Time.time + GetCheckInterval(); // 값을 갱신할거에요 -> 다음 검사 예정 시간을
} }
} }
private void ValidateConfiguration() private void ValidateConfiguration() // 함수를 선언할거에요 -> 설정 누락 여부를 확인하는 ValidateConfiguration을
{ {
if (_config == null) _config = RenderOptimizationConfig.CreateDefault(); if (_config == null) // 조건이 맞으면 실행할거에요 -> 설정 에셋이 비어있다면
}
public void InitializeSystem()
{ {
if (!FindPlayer()) return; Debug.LogWarning("[PlayerRangeManager] Config가 없습니다! 기본값을 생성합니다."); // 경고를 출력할거에요 -> 자동 생성 메시지를
if (!ScanEnvironmentObjects()) return; _config = RenderOptimizationConfig.CreateDefault(); // 값을 설정할거에요 -> 기본값으로 생성된 에셋을
_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 (parent == null) continue;
// ⭐ 폴더 구조가 몇 층이든 상관없이 모든 Renderer를 낱낱이 찾아냅니다.
Renderer[] allRenderers = parent.GetComponentsInChildren<Renderer>(_config.IncludeInactiveObjects);
foreach (Renderer r in allRenderers)
{
if (r == null) continue;
// 각 렌더러를 독립적인 그룹으로 생성 (개별 최적화의 핵심)
_renderGroups.Add(new RenderGroup(new Renderer[] { r }));
} }
} }
_totalObjectCount = _renderGroups.Count; private void InitializeSystem() // 함수를 선언할거에요 -> 전체 시스템을 초기화하는 InitializeSystem을
return _totalObjectCount > 0; {
FindPlayer(); // 함수를 실행할거에요 -> 플레이어를 찾는 기능을
ScanEnvironmentObjects(); // 함수를 실행할거에요 -> 주변 오브젝트를 스캔하는 기능을
_isInitialized = true; // 상태를 바꿀거에요 -> 초기화 완료인 참(true)으로
if (ShouldLog()) Debug.Log($"[PlayerRangeManager] 초기화 완료. 대상 그룹: {_totalObjectCount}"); // 조건이 맞으면 로그를 출력할거에요 -> 결과 보고 메시지를
} }
private void UpdateCulling() private void FindPlayer() // 함수를 선언할거에요 -> 플레이어를 탐색하는 FindPlayer를
{ {
if (_playerTransform == null) return; GameObject player = GameObject.FindGameObjectWithTag(GetPlayerTag()); // 오브젝트를 찾을거에요 -> 지정된 태그를 가진 플레이어를
if (player != null) // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면
Vector3 playerPosition = _playerTransform.position;
float maxDistance = GetRenderRange();
int changedCount = 0;
int visibleCount = 0;
for (int i = 0; i < _renderGroups.Count; i++)
{ {
if (_renderGroups[i].UpdateVisibility(playerPosition, maxDistance)) changedCount++; _playerTransform = player.transform; // 값을 저장할거에요 -> 플레이어의 Transform 정보를
if (_renderGroups[i].IsVisible) visibleCount++; }
else // 조건이 틀리면 실행할거에요 -> 플레이어를 못 찾았다면
{
Debug.LogError($"[PlayerRangeManager] '{GetPlayerTag()}' 태그의 플레이어를 찾을 수 없습니다!"); // 에러를 출력할거에요 -> 태그 불일치 메시지를
}
} }
_visibleObjectCount = visibleCount; private void ScanEnvironmentObjects() // 함수를 선언할거에요 -> 환경 오브젝트들을 스캔하는 ScanEnvironmentObjects를
_lastFrameChangedCount = changedCount; {
_renderGroups.Clear(); // 리스트를 비울거에요 -> 기존에 있던 렌더 그룹들을
foreach (var parent in _environmentParents) // 반복할거에요 -> 등록된 모든 부모 폴더에 대해
{
if (parent == null) continue; // 조건이 맞으면 건너뛸거에요 -> 부모가 비어있다면
foreach (Transform child in parent) // 반복할거에요 -> 부모 폴더의 모든 자식 오브젝트에 대해
{
Renderer[] renderers = child.GetComponentsInChildren<Renderer>(true); // 배열을 가져올거에요 -> 자식 내부의 모든 렌더러들을
if (renderers.Length > 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재한다면
{
_renderGroups.Add(new RenderGroup(renderers)); // 리스트에 추가할거에요 -> 새로운 렌더 그룹을 생성해서
}
}
} }
public void RefreshObjectList() { ScanEnvironmentObjects(); } _totalObjectCount = _renderGroups.Count; // 값을 저장할거에요 -> 총 관리 대상 개수를
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 void OnDrawGizmos()
{
if (!ShouldShowGizmos() || _playerTransform == null) return;
Gizmos.color = new Color(1f, 0f, 0f, 0.3f);
Gizmos.DrawWireSphere(_playerTransform.position, GetRenderRange());
} }
private void OnDrawGizmosSelected() private void UpdateCulling() // 함수를 선언할거에요 -> 컬링을 업데이트하는 UpdateCulling을
{ {
if (!ShouldShowGizmos() || !_isInitialized) return; Vector3 playerPos = _playerTransform.position; // 값을 가져올거에요 -> 현재 플레이어의 위치를
foreach (var group in _renderGroups) float range = GetRenderRange(); // 값을 가져올거에요 -> 설정된 렌더링 거리 수치를
int changedCount = 0; // 변수를 초기화할거에요 -> 변경 개수를 0으로
int visibleCount = 0; // 변수를 초기화할거에요 -> 표시 중인 개수를 0으로
for (int i = 0; i < _renderGroups.Count; i++) // 반복할거에요 -> 모든 렌더 그룹에 대해
{ {
Gizmos.color = group.IsVisible ? new Color(0f, 1f, 0f, 0.5f) : new Color(0.5f, 0.5f, 0.5f, 0.2f); if (_renderGroups[i].UpdateVisibility(playerPos, range)) changedCount++; // 조건이 맞으면 증가시킬거에요 -> 상태가 바뀌었다면 변경 수를
Gizmos.DrawSphere(group.ActualCenter, 0.3f); if (_renderGroups[i].IsVisible) visibleCount++; // 조건이 맞으면 증가시킬거에요 -> 현재 보이고 있다면 표시 수를
}
_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; // 값을 바꿀거에요 -> 일시정지 여부를 전달받은 인자대로
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() // 함수를 실행할거에요 -> 기즈모를 그리는 OnDrawGizmos를
{
if (!ShouldShowGizmos() || _playerTransform == null) return; // 조건이 맞으면 중단할거에요 -> 표시 설정이 꺼졌거나 플레이어가 없다면
Gizmos.color = new Color(1f, 0f, 0f, 0.3f); // 색상을 설정할거에요 -> 빨간색 반투명으로
Gizmos.DrawWireSphere(_playerTransform.position, GetRenderRange()); // 그림을 그릴거에요 -> 플레이어 중심의 사거리 구체를
}
private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 선택 시 기즈모를 그리는 OnDrawGizmosSelected를
{
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.DrawWireSphere(group.ActualCenter, group.BoundingRadius); // 그림을 그릴거에요 -> 각 그룹의 경계면 구체를
} }
} }
} }

View File

@ -1,20 +1,20 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
namespace GameSystems.Optimization namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로
{ {
/// <summary> /// <summary>
/// 렌더링 최적화를 위한 오브젝트 그룹 데이터 /// 렌더링 최적화를 위한 오브젝트 그룹 데이터
/// - 불변성(Immutability) 보장 /// - 불변성(Immutability) 보장
/// - 캡슐화된 내부 데이터 /// - 캡슐화된 내부 데이터
/// </summary> /// </summary>
public sealed class RenderGroup public sealed class RenderGroup // 클래스를 선언할거에요 -> 상속 불가능한 최적화 그룹인 RenderGroup을
{ {
#region Private Fields #region Private Fields
private readonly Vector3 _actualCenter; private readonly Vector3 _actualCenter; // 변수를 선언할거에요 -> 읽기 전용으로 실제 중심 좌표를
private readonly Renderer[] _renderers; private readonly Renderer[] _renderers; // 배열을 선언할거에요 -> 읽기 전용으로 렌더러 목록을
private readonly float _boundingRadius; private readonly float _boundingRadius; // 변수를 선언할거에요 -> 읽기 전용으로 경계 반지름 수치를
private bool _isCurrentlyVisible; private bool _isCurrentlyVisible; // 변수를 선언할거에요 -> 현재 표시 상태를 저장할 변수를
#endregion #endregion
@ -23,22 +23,22 @@ namespace GameSystems.Optimization
/// <summary> /// <summary>
/// 실제 메쉬의 중심점 (피벗이 아닌 bounds 기준) /// 실제 메쉬의 중심점 (피벗이 아닌 bounds 기준)
/// </summary> /// </summary>
public Vector3 ActualCenter => _actualCenter; public Vector3 ActualCenter => _actualCenter; // 프로퍼티를 선언할거에요 -> 실제 중심점을 반환하는 기능을
/// <summary> /// <summary>
/// 오브젝트의 최대 반지름 (경계 구체) /// 오브젝트의 최대 반지름 (경계 구체)
/// </summary> /// </summary>
public float BoundingRadius => _boundingRadius; public float BoundingRadius => _boundingRadius; // 프로퍼티를 선언할거에요 -> 경계 반지름을 반환하는 기능을
/// <summary> /// <summary>
/// 현재 렌더링 상태 /// 현재 렌더링 상태
/// </summary> /// </summary>
public bool IsVisible => _isCurrentlyVisible; public bool IsVisible => _isCurrentlyVisible; // 프로퍼티를 선언할거에요 -> 표시 여부 상태를 반환하는 기능을
/// <summary> /// <summary>
/// 관리 중인 Renderer 개수 /// 관리 중인 Renderer 개수
/// </summary> /// </summary>
public int RendererCount => _renderers?.Length ?? 0; public int RendererCount => _renderers?.Length ?? 0; // 프로퍼티를 선언할거에요 -> 렌더러 개수를 반환하는 기능을 (null이면 0)
#endregion #endregion
@ -48,28 +48,28 @@ namespace GameSystems.Optimization
/// RenderGroup 생성자 /// RenderGroup 생성자
/// </summary> /// </summary>
/// <param name="renderers">대상 Renderer 배열 (null 체크 수행)</param> /// <param name="renderers">대상 Renderer 배열 (null 체크 수행)</param>
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 또는 비어있습니다."); Debug.LogError("[RenderGroup] Renderer 배열이 null 또는 비어있습니다."); // 에러를 출력할거에요 -> 로그 메시지를
_renderers = System.Array.Empty<Renderer>(); _renderers = System.Array.Empty<Renderer>(); // 값을 할당할거에요 -> 빈 렌더러 배열을
_actualCenter = Vector3.zero; _actualCenter = Vector3.zero; // 값을 할당할거에요 -> 제로 벡터를 중심점으로
_boundingRadius = 0f; _boundingRadius = 0f; // 값을 할당할거에요 -> 0을 반지름으로
return; return; // 중단할거에요 -> 초기화 로직을
} }
// 방어적 복사 (외부에서 배열 수정 방지) // 방어적 복사 (외부에서 배열 수정 방지)
_renderers = new Renderer[renderers.Length]; _renderers = new Renderer[renderers.Length]; // 배열을 생성할거에요 -> 같은 크기의 새로운 렌더러 배열을
System.Array.Copy(renderers, _renderers, renderers.Length); System.Array.Copy(renderers, _renderers, renderers.Length); // 값을 복사할거에요 -> 원본 렌더러들을 내부 배열로
// 실제 중심점 계산 // 실제 중심점 계산
_actualCenter = CalculateGroupCenter(_renderers); _actualCenter = CalculateGroupCenter(_renderers); // 값을 계산해서 넣을거에요 -> 그룹의 통합 중심점을
// 경계 반지름 계산 // 경계 반지름 계산
_boundingRadius = CalculateBoundingRadius(_renderers, _actualCenter); _boundingRadius = CalculateBoundingRadius(_renderers, _actualCenter); // 값을 계산해서 넣을거에요 -> 최대 경계 반지름을
_isCurrentlyVisible = true; // 초기 상태는 표시 _isCurrentlyVisible = true; // 초기값을 설정할거에요 -> 표시 상태를 참(true)으로
} }
#endregion #endregion
@ -82,58 +82,58 @@ namespace GameSystems.Optimization
/// <param name="referencePosition">기준 위치 (플레이어 등)</param> /// <param name="referencePosition">기준 위치 (플레이어 등)</param>
/// <param name="maxDistance">최대 렌더링 거리</param> /// <param name="maxDistance">최대 렌더링 거리</param>
/// <returns>상태가 변경되었는지 여부</returns> /// <returns>상태가 변경되었는지 여부</returns>
public bool UpdateVisibility(Vector3 referencePosition, float maxDistance) public bool UpdateVisibility(Vector3 referencePosition, float maxDistance) // 함수를 선언할거에요 -> 거리에 따른 표시 갱신 기능을
{ {
// 거리 계산 (오브젝트 크기 고려) // 거리 계산 (오브젝트 크기 고려)
float distance = Vector3.Distance(referencePosition, _actualCenter); float distance = Vector3.Distance(referencePosition, _actualCenter); // 거리를 계산할거에요 -> 기준점과 그룹 중심 사이의
bool shouldBeVisible = (distance - _boundingRadius) <= maxDistance; bool shouldBeVisible = (distance - _boundingRadius) <= maxDistance; // 상태를 판단할거에요 -> 사거리와 반지름을 고려해서 표시 여부를
// 상태 변경이 필요한 경우에만 처리 // 상태 변경이 필요한 경우에만 처리
if (shouldBeVisible == _isCurrentlyVisible) if (shouldBeVisible == _isCurrentlyVisible) // 조건이 맞으면 실행할거에요 -> 이전 상태와 동일하다면
{ {
return false; // 변경 없음 return false; // 반환할거에요 -> 변경되지 않았음을
} }
// Renderer 상태 일괄 변경 // Renderer 상태 일괄 변경
SetRenderersState(shouldBeVisible); SetRenderersState(shouldBeVisible); // 함수를 실행할거에요 -> 모든 렌더러의 활성화 상태를 바꾸는 기능을
_isCurrentlyVisible = shouldBeVisible; _isCurrentlyVisible = shouldBeVisible; // 값을 갱신할거에요 -> 현재 표시 상태를
return true; // 변경됨 return true; // 반환할거에요 -> 상태가 변경되었음을
} }
/// <summary> /// <summary>
/// 강제로 모든 Renderer 표시 /// 강제로 모든 Renderer 표시
/// </summary> /// </summary>
public void ForceShow() public void ForceShow() // 함수를 선언할거에요 -> 강제 표시 기능인 ForceShow를
{ {
SetRenderersState(true); SetRenderersState(true); // 함수를 실행할거에요 -> 모든 렌더러를 켜는 기능을
_isCurrentlyVisible = true; _isCurrentlyVisible = true; // 상태를 설정할거에요 -> 표시 중인 것으로
} }
/// <summary> /// <summary>
/// 강제로 모든 Renderer 숨김 /// 강제로 모든 Renderer 숨김
/// </summary> /// </summary>
public void ForceHide() public void ForceHide() // 함수를 선언할거에요 -> 강제 숨김 기능인 ForceHide를
{ {
SetRenderersState(false); SetRenderersState(false); // 함수를 실행할거에요 -> 모든 렌더러를 끄는 기능을
_isCurrentlyVisible = false; _isCurrentlyVisible = false; // 상태를 설정할거에요 -> 숨김 중인 것으로
} }
/// <summary> /// <summary>
/// 그룹 유효성 검사 (Renderer가 파괴되었는지 확인) /// 그룹 유효성 검사 (Renderer가 파괴되었는지 확인)
/// </summary> /// </summary>
public bool IsValid() public bool IsValid() // 함수를 선언할거에요 -> 유효성 검사 기능인 IsValid를
{ {
if (_renderers == null || _renderers.Length == 0) if (_renderers == null || _renderers.Length == 0) // 조건이 맞으면 반환할거에요 -> 배열이 비어있다면 거짓(false)을
return false; return false; // 반환할거에요 -> 유효하지 않음을
// 하나라도 유효한 Renderer가 있으면 true // 하나라도 유효한 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 #endregion
@ -143,73 +143,73 @@ namespace GameSystems.Optimization
/// <summary> /// <summary>
/// 여러 Renderer의 통합 중심점 계산 /// 여러 Renderer의 통합 중심점 계산
/// </summary> /// </summary>
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를 포함하는 통합 경계 계산
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; Bounds temp = combinedBounds.Value; // 변수를 설정할거에요 -> 임시 경계값으로
temp.Encapsulate(r.bounds); temp.Encapsulate(r.bounds); // 영역을 확장할거에요 -> 새로운 렌더러를 포함하도록
combinedBounds = temp; combinedBounds = temp; // 값을 갱신할거에요 -> 확장된 경계값으로
} }
} }
return combinedBounds?.center ?? Vector3.zero; return combinedBounds?.center ?? Vector3.zero; // 반환할거에요 -> 계산된 중심점을 (없으면 제로 벡터)
} }
/// <summary> /// <summary>
/// 오브젝트의 최대 반지름 계산 /// 오브젝트의 최대 반지름 계산
/// </summary> /// </summary>
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 크기 // 각 Renderer 중심에서의 거리 + extent 크기
float distanceFromCenter = Vector3.Distance(r.bounds.center, center); float distanceFromCenter = Vector3.Distance(r.bounds.center, center); // 거리를 계산할거에요 -> 렌더러 중심과 그룹 중심 사이의
float extent = r.bounds.extents.magnitude; float extent = r.bounds.extents.magnitude; // 크기를 가져올거에요 -> 경계 상자의 대각선 길이를
float totalRadius = distanceFromCenter + extent; float totalRadius = distanceFromCenter + extent; // 값을 합산할거에요 -> 거리와 크기를 더해서
if (totalRadius > maxRadius) if (totalRadius > maxRadius) // 조건이 맞으면 실행할거에요 -> 현재 합계가 더 크다면
{ {
maxRadius = totalRadius; maxRadius = totalRadius; // 값을 갱신할거에요 -> 새로운 최대 반지름으로
} }
} }
return maxRadius; return maxRadius; // 반환할거에요 -> 최종 계산된 최대 반지름을
} }
/// <summary> /// <summary>
/// 모든 Renderer의 enabled 상태 일괄 변경 /// 모든 Renderer의 enabled 상태 일괄 변경
/// </summary> /// </summary>
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 체크 + 상태가 다를 때만 변경 (성능 최적화) // null 체크 + 상태가 다를 때만 변경 (성능 최적화)
if (r != null && r.enabled != enabled) if (r != null && r.enabled != enabled) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재하고 상태 변경이 필요하다면
{ {
r.enabled = enabled; r.enabled = enabled; // 설정을 바꿀거에요 -> 활성화 여부를 인자값대로
} }
} }
} }
@ -221,10 +221,10 @@ namespace GameSystems.Optimization
/// <summary> /// <summary>
/// 리소스 정리 (명시적 호출용) /// 리소스 정리 (명시적 호출용)
/// </summary> /// </summary>
public void Dispose() public void Dispose() // 함수를 선언할거에요 -> 자원 정리 기능을
{ {
// Renderer 배열 초기화 (GC 도움) // Renderer 배열 초기화 (GC 도움)
System.Array.Clear(_renderers, 0, _renderers.Length); System.Array.Clear(_renderers, 0, _renderers.Length); // 비울거에요 -> 렌더러 배열의 모든 요소를
} }
#endregion #endregion

View File

@ -1,94 +1,81 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
namespace GameSystems.Optimization namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로
{ {
/// <summary> /// <summary>
/// PlayerRangeManager의 설정을 관리하는 ScriptableObject /// PlayerRangeManager의 설정을 관리하는 ScriptableObject
/// - 프로젝트 전역 설정 공유
/// - 런타임 수정 불가 (데이터 무결성 보장)
/// </summary> /// </summary>
[CreateAssetMenu(fileName = "RenderOptimizationConfig", menuName = "Game Systems/Render Optimization Config")] [CreateAssetMenu(fileName = "RenderOptimizationConfig", menuName = "Game Systems/Render Optimization Config")] // 에셋 생성 메뉴를 만들거에요 -> 지정된 경로에
public sealed class RenderOptimizationConfig : ScriptableObject public sealed class RenderOptimizationConfig : ScriptableObject // 클래스를 선언할거에요 -> ScriptableObject를 상속받는 RenderOptimizationConfig를
{ {
#region Inspector Fields #region Inspector Fields
[Header("=== 렌더링 범위 설정 ===")] [Header("=== 렌더링 범위 설정 ===")] // 인스펙터에 제목을 표시할거에요 -> === 렌더링 범위 설정 === 을
[Tooltip("플레이어 주변 렌더링 반지름 (유니티 단위)")] [Tooltip("플레이어 주변 렌더링 반지름 (유니티 단위)")] // 툴팁을 추가할거에요 -> 설명 문구를
[SerializeField, Range(10f, 200f)] [SerializeField, Range(10f, 200f)] // 슬라이더 범위를 지정할거에요 -> 10에서 200 사이로
private float _renderRange = 40f; private float _renderRange = 40f; // 변수를 선언할거에요 -> 기본 렌더링 범위인 40을 _renderRange에
[Tooltip("페이드 아웃 시작 거리 비율 (0.8 = 렌더 범위의 80%부터 페이드 시작)")] [Tooltip("페이드 아웃 시작 거리 비율 (0.8 = 렌더 범위의 80%부터 페이드 시작)")] // 툴팁을 추가할거에요 -> 비율 설명을
[SerializeField, Range(0.5f, 1f)] [SerializeField, Range(0.5f, 1f)] // 슬라이더 범위를 지정할거에요 -> 0.5에서 1 사이로
private float _fadeStartRatio = 0.8f; private float _fadeStartRatio = 0.8f; // 변수를 선언할거에요 -> 페이드 시작 비율인 0.8을 _fadeStartRatio에
[Header("=== 업데이트 주기 ===")] [Header("=== 업데이트 주기 ===")] // 제목을 표시할거에요 -> === 업데이트 주기 === 를
[Tooltip("오브젝트 컬링 검사 주기 (초 단위)")] [Tooltip("오브젝트 컬링 검사 주기 (초 단위)")] // 툴팁을 추가할거에요 -> 검사 주기 설명을
[SerializeField, Range(0.05f, 1f)] [SerializeField, Range(0.05f, 1f)] // 슬라이더 범위를 지정할거에요 -> 0.05에서 1 사이로
private float _checkInterval = 0.2f; private float _checkInterval = 0.2f; // 변수를 선언할거에요 -> 검사 간격인 0.2를 _checkInterval에
[Header("=== 플레이어 설정 ===")] [Header("=== 플레이어 설정 ===")] // 제목을 표시할거에요 -> === 플레이어 설정 === 을
[Tooltip("플레이어를 찾을 태그 이름")] [Tooltip("플레이어를 찾을 태그 이름")] // 툴팁을 추가할거에요 -> 태그 이름 설명을
[SerializeField] [SerializeField] // 필드를 직렬화할거에요 -> 인스펙터에 보이게
private string _playerTag = "Player"; private string _playerTag = "Player"; // 변수를 선언할거에요 -> 플레이어 태그인 "Player"를 _playerTag에
[Header("=== 성능 설정 ===")] [Header("=== 성능 설정 ===")] // 제목을 표시할거에요 -> === 성능 설정 === 을
[Tooltip("한 프레임에 처리할 최대 오브젝트 수 (0 = 무제한)")] [Tooltip("한 프레임에 처리할 최대 오브젝트 수 (0 = 무제한)")] // 툴팁을 추가할거에요 -> 처리량 설명을
[SerializeField, Range(0, 500)] [SerializeField, Min(0)] // 최소값을 지정할거에요 -> 0으로
private int _maxObjectsPerFrame = 0; private int _maxObjectsPerFrame = 50; // 변수를 선언할거에요 -> 프레임당 최대 개수인 50을 _maxObjectsPerFrame에
[Tooltip("비활성 오브젝트도 스캔할지 여부")] [Header("=== 디버그 설정 ===")] // 제목을 표시할거에요 -> === 디버그 설정 === 을
[SerializeField] [SerializeField] private bool _showGizmos = true; // 변수를 선언할거에요 -> 기즈모 표시 여부를 _showGizmos에
private bool _includeInactiveObjects = true; [SerializeField] private bool _enableVerboseLogging = false; // 변수를 선언할거에요 -> 상세 로그 여부를 _enableVerboseLogging에
[Header("=== 디버그 설정 ===")]
[Tooltip("기즈모 표시 여부")]
[SerializeField]
private bool _showGizmos = true;
[Tooltip("상세 로그 출력")]
[SerializeField]
private bool _enableVerboseLogging = false;
#endregion #endregion
#region Public Properties (Immutable) #region Public Properties
public float RenderRange => _renderRange; public float RenderRange => _renderRange; // 값을 반환할거에요 -> 렌더링 범위 수치를
public float FadeStartRatio => _fadeStartRatio; public float CheckInterval => _checkInterval; // 값을 반환할거에요 -> 검사 주기 수치를
public float CheckInterval => _checkInterval; public string PlayerTag => _playerTag; // 값을 반환할거에요 -> 플레이어 태그 문자열을
public string PlayerTag => _playerTag; public int MaxObjectsPerFrame => _maxObjectsPerFrame; // 값을 반환할거에요 -> 최대 처리 개수를
public int MaxObjectsPerFrame => _maxObjectsPerFrame; public bool ShowGizmos => _showGizmos; // 값을 반환할거에요 -> 기즈모 활성화 여부를
public bool IncludeInactiveObjects => _includeInactiveObjects; public bool EnableVerboseLogging => _enableVerboseLogging; // 값을 반환할거에요 -> 로그 활성화 여부를
public bool ShowGizmos => _showGizmos;
public bool EnableVerboseLogging => _enableVerboseLogging;
/// <summary> /// <summary>
/// 페이드 시작 거리 계산 /// 페이드 시작 거리 계산
/// </summary> /// </summary>
public float FadeStartDistance => _renderRange * _fadeStartRatio; public float FadeStartDistance => _renderRange * _fadeStartRatio; // 값을 계산해서 반환할거에요 -> 렌더링 범위에 비율을 곱한 결과를
#endregion #endregion
#region Validation #region Validation
private void OnValidate() private void OnValidate() // 값이 변경될 때 실행할거에요 -> OnValidate 함수를
{ {
// 유효성 검사 // 유효성 검사
if (_renderRange < 10f) if (_renderRange < 10f) // 조건이 맞으면 실행할거에요 -> 렌더링 범위가 10보다 작다면
{ {
Debug.LogWarning("[RenderOptimizationConfig] Render Range가 너무 작습니다. 최소 10으로 설정됩니다."); Debug.LogWarning("[RenderOptimizationConfig] Render Range가 너무 작습니다. 최소 10으로 설정됩니다."); // 경고를 출력할거에요 -> 범위 오류 메시지를
_renderRange = 10f; _renderRange = 10f; // 값을 설정할거에요 -> 10으로
} }
if (string.IsNullOrWhiteSpace(_playerTag)) if (string.IsNullOrWhiteSpace(_playerTag)) // 조건이 맞으면 실행할거에요 -> 플레이어 태그가 비어있다면
{ {
Debug.LogWarning("[RenderOptimizationConfig] Player Tag가 비어있습니다. 'Player'로 설정됩니다."); Debug.LogWarning("[RenderOptimizationConfig] Player Tag가 비어있습니다. 'Player'로 설정됩니다."); // 경고를 출력할거에요 -> 태그 오류 메시지를
_playerTag = "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,14 +86,14 @@ namespace GameSystems.Optimization
/// <summary> /// <summary>
/// 기본 설정 생성 /// 기본 설정 생성
/// </summary> /// </summary>
public static RenderOptimizationConfig CreateDefault() public static RenderOptimizationConfig CreateDefault() // 함수를 선언할거에요 -> 기본 설정을 생성하는 CreateDefault를
{ {
RenderOptimizationConfig config = CreateInstance<RenderOptimizationConfig>(); RenderOptimizationConfig config = CreateInstance<RenderOptimizationConfig>(); // 인스턴스를 생성할거에요 -> RenderOptimizationConfig 객체를
config._renderRange = 40f; config._renderRange = 40f; // 값을 설정할거에요 -> 40으로
config._checkInterval = 0.2f; config._checkInterval = 0.2f; // 값을 설정할거에요 -> 0.2로
config._playerTag = "Player"; config._playerTag = "Player"; // 값을 설정할거에요 -> "Player"로
config._showGizmos = true; config._showGizmos = true; // 값을 설정할거에요 -> 참(true)으로
return config; return config; // 반환할거에요 -> 생성된 설정 객체를
} }
#endregion #endregion

View File

@ -1,14 +1,14 @@
using System.Collections; using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를
using System.Collections.Generic; using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement; // 씬 관리 기능을 사용할거에요 -> UnityEngine.SceneManagement를
public class LoadScene : MonoBehaviour public class LoadScene : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 LoadScene을
{ {
[SerializeField] private string sceneName; [SerializeField] private string sceneName; // 변수를 선언할거에요 -> 로드할 씬 이름을 저장할 sceneName을
public void LoadOtherScene() public void LoadOtherScene() // 함수를 선언할거에요 -> 다른 씬을 로드하는 LoadOtherScene을
{ {
SceneManager.LoadScene(sceneName); SceneManager.LoadScene(sceneName); // 실행할거에요 -> 지정된 이름의 씬을 로드하는 기능을
} }
} }

View File

@ -1,22 +1,52 @@
using System.Collections; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections.Generic; using UnityEngine.SceneManagement; // 씬 관리 기능을 사용할거에요 -> UnityEngine.SceneManagement를
using UnityEngine;
public class GameStopPanelController : MonoBehaviour public class GameStopPanelController : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 GameStopPanelController를
{ {
[SerializeField] private GameObject gameStopPanel; [Header("UI 요소")] // 인스펙터 창에 제목을 표시할거에요 -> UI 요소 를
[SerializeField] private SettingPanelController settingPanelController; [SerializeField] private GameObject stopPanel; // 변수를 선언할거에요 -> 일시정지 패널 오브젝트인 stopPanel을
private bool isGameStopping = false;
private void Update() private bool isPaused = false; // 변수를 초기화할거에요 -> 일시정지 상태 여부를 거짓으로
{
if (settingPanelController != null && settingPanelController.IsSettingOpen)
return;
if (Input.GetKeyDown(KeyCode.Escape)) private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
{ {
isGameStopping = !isGameStopping; // ESC 키 입력 감지
gameStopPanel.SetActive(isGameStopping); if (Input.GetKeyDown(KeyCode.Escape)) // 조건이 맞으면 실행할거에요 -> ESC 키를 눌렀다면
{
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 // 조건부 컴파일을 끝낼거에요
}
} }

View File

@ -1,19 +1,50 @@
using System.Collections; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections.Generic; using UnityEngine.UI; // 유니티 UI 기능을 사용할거에요 -> UnityEngine.UI를
using TMPro; using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
using UnityEngine;
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); // 실행할거에요 -> 화질 설정을 적용하는 함수를
} }
} }

View File

@ -1,37 +1,50 @@
using System.Collections; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections.Generic; using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
using TMPro; using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
using UnityEngine;
using UnityEngine.UI;
public class SettingResolutionUI : MonoBehaviour public class SettingResolutionUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 SettingResolutionUI를
{ {
[SerializeField] private TMP_Dropdown resDropDown; [SerializeField] private TMP_Dropdown resolutionDropdown; // 변수를 선언할거에요 -> 해상도 선택 드롭다운 UI를 resolutionDropdown에
[SerializeField] private Toggle fullScreenToggle;
private void Start() private Resolution[] resolutions; // 배열을 선언할거에요 -> 사용 가능한 해상도 목록을 resolutions에
private void Start() // 함수를 실행할거에요 -> 스크립트 시작 시 Start를
{ {
var manager = SettingsManager.Instance; InitDropdown(); // 함수를 실행할거에요 -> 드롭다운을 초기화하는 InitDropdown을
resDropDown.ClearOptions();
var options = new List<string>();
foreach (var res in manager.FilteredResolutions)
options.Add($"{res.width} x {res.height}");
resDropDown.AddOptions(options);
resDropDown.value = manager.resolutionIndex;
fullScreenToggle.isOn = manager.isFullScreen;
} }
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<string> options = new List<string>(); // 리스트를 생성할거에요 -> 옵션 문자열들을 담을 리스트를
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; // 값을 저장할거에요 -> 현재 인덱스로
}
} }
public void OnFullScreenChanged(bool value) resolutionDropdown.AddOptions(options); // 추가할거에요 -> 완성된 옵션 리스트를 드롭다운에
resolutionDropdown.value = currentResolutionIndex; // 값을 설정할거에요 -> 현재 선택된 해상도로
resolutionDropdown.RefreshShownValue(); // 갱신할거에요 -> 드롭다운 표시 내용을
resolutionDropdown.onValueChanged.AddListener(OnResolutionChanged); // 구독할거에요 -> 값 변경 이벤트를
}
public void OnResolutionChanged(int index) // 함수를 선언할거에요 -> 해상도 변경 시 호출될 OnResolutionChanged를
{ {
SettingsManager.Instance.SetFullScreen(value); if (SettingsManager.Instance != null) // 조건이 맞으면 실행할거에요 -> 매니저가 있다면
SettingsManager.Instance.SetResolution(index); // 실행할거에요 -> 해상도 설정을 적용하는 함수를
} }
} }

View File

@ -1,20 +1,20 @@
using System.Collections; using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를
using System.Collections.Generic; using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.SceneManagement; 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 qualityIndex; // 변수를 선언할거에요 -> 그래픽 품질 인덱스를 qualityIndex에
public int resolutionIndex; public int resolutionIndex; // 변수를 선언할거에요 -> 해상도 인덱스를 resolutionIndex에
public bool isFullScreen; public bool isFullScreen; // 변수를 선언할거에요 -> 전체 화면 여부를 isFullScreen에
private Resolution[] allResolutions; private Resolution[] allResolutions; // 배열을 선언할거에요 -> 지원하는 모든 해상도를 담을 allResolutions를
public Resolution[] FilteredResolutions { get; private set; } public Resolution[] FilteredResolutions { get; private set; } // 프로퍼티를 선언할거에요 -> 필터링된 해상도 목록을 외부에서 읽기만 가능하게
private readonly Vector2Int[] commonResolutions = private readonly Vector2Int[] commonResolutions = // 배열을 초기화할거에요 -> 공통 해상도 목록을 읽기 전용으로
{ {
new(1280, 720), new(1280, 720),
new(1280, 768), new(1280, 768),
@ -25,127 +25,127 @@ public class SettingsManager : MonoBehaviour
new(1920, 1080) 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); Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를
return; return; // 중단할거에요 -> 초기화 로직을
} }
Instance = this; Instance = this; // 값을 저장할거에요 -> 나 자신을 싱글톤 인스턴스에
DontDestroyOnLoad(gameObject); DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬이 바뀌어도 파괴되지 않게
InitResolutions(); InitResolutions(); // 함수를 실행할거에요 -> 해상도 목록을 초기화하는 InitResolutions를
LoadSettings(); 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<Resolution>(); var list = new System.Collections.Generic.List<Resolution>(); // 리스트를 생성할거에요 -> 해상도를 담을 임시 리스트를
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)) if (!list.Exists(r => r.width == res.width && r.height == res.height)) // 조건이 맞으면 실행할거에요 -> 리스트에 없는 해상도라면 (중복 방지)
list.Add(res); list.Add(res); // 추가할거에요 -> 리스트에 해당 해상도를
} }
} }
} }
FilteredResolutions = list.ToArray(); FilteredResolutions = list.ToArray(); // 변환할거에요 -> 리스트를 배열로 바꿔서 FilteredResolutions에
} }
// ===== 적용 메서드 ===== // ===== 적용 메서드 =====
public void SetQuality(int index) public void SetQuality(int index) // 함수를 선언할거에요 -> 그래픽 품질을 설정하는 SetQuality를
{ {
qualityIndex = index; qualityIndex = index; // 값을 저장할거에요 -> 품질 인덱스를
QualitySettings.SetQualityLevel(index); QualitySettings.SetQualityLevel(index); // 설정을 바꿀거에요 -> 유니티 품질 설정을 해당 레벨로
PlayerPrefs.SetInt("Quality", index); PlayerPrefs.SetInt("Quality", index); // 저장할거에요 -> 품질 인덱스를 로컬 저장소에
} }
public void SetResolution(int index) public void SetResolution(int index) // 함수를 선언할거에요 -> 해상도를 설정하는 SetResolution을
{ {
resolutionIndex = index; resolutionIndex = index; // 값을 저장할거에요 -> 해상도 인덱스를
ApplyResolution(); ApplyResolution(); // 함수를 실행할거에요 -> 해상도를 적용하는 ApplyResolution을
PlayerPrefs.SetInt("Resolution", index); PlayerPrefs.SetInt("Resolution", index); // 저장할거에요 -> 해상도 인덱스를 로컬 저장소에
} }
public void SetFullScreen(bool full) public void SetFullScreen(bool full) // 함수를 선언할거에요 -> 전체 화면을 설정하는 SetFullScreen을
{ {
isFullScreen = full; isFullScreen = full; // 값을 저장할거에요 -> 전체 화면 여부를
Screen.fullScreenMode = full Screen.fullScreenMode = full // 설정을 바꿀거에요 -> 화면 모드를
? FullScreenMode.FullScreenWindow ? FullScreenMode.FullScreenWindow // 조건이 참이면 전체 화면 창 모드로
: FullScreenMode.Windowed; : FullScreenMode.Windowed; // 조건이 거짓이면 창 모드로
ApplyResolution(); ApplyResolution(); // 함수를 실행할거에요 -> 해상도 적용 함수를
PlayerPrefs.SetInt("FullScreen", full ? 1 : 0); PlayerPrefs.SetInt("FullScreen", full ? 1 : 0); // 저장할거에요 -> 전체 화면 여부를 (1:참, 0:거짓)
} }
void ApplyResolution() void ApplyResolution() // 함수를 선언할거에요 -> 해상도를 실제 적용하는 ApplyResolution을
{ {
var res = FilteredResolutions[resolutionIndex]; var res = FilteredResolutions[resolutionIndex]; // 값을 가져올거에요 -> 선택된 해상도 정보를
Screen.SetResolution( Screen.SetResolution( // 설정을 바꿀거에요 -> 화면 해상도를
res.width, res.width, // 가로 크기로
res.height, res.height, // 세로 크기로
isFullScreen ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed isFullScreen ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed // 화면 모드에 맞춰서
); );
} }
void LoadSettings() void LoadSettings() // 함수를 선언할거에요 -> 저장된 설정을 불러오는 LoadSettings를
{ {
qualityIndex = PlayerPrefs.GetInt("Quality", QualitySettings.names.Length - 1); qualityIndex = PlayerPrefs.GetInt("Quality", QualitySettings.names.Length - 1); // 불러올거에요 -> 품질 인덱스를 (없으면 최대값)
resolutionIndex = PlayerPrefs.GetInt("Resolution", 0); resolutionIndex = PlayerPrefs.GetInt("Resolution", 0); // 불러올거에요 -> 해상도 인덱스를 (없으면 0)
isFullScreen = PlayerPrefs.GetInt("FullScreen", 1) == 1; isFullScreen = PlayerPrefs.GetInt("FullScreen", 1) == 1; // 불러올거에요 -> 전체 화면 여부를 (없으면 1=참)
} }
void ApplySettings() void ApplySettings() // 함수를 선언할거에요 -> 모든 설정을 적용하는 ApplySettings를
{ {
SetQuality(qualityIndex); SetQuality(qualityIndex); // 실행할거에요 -> 품질 설정을
Screen.fullScreenMode = isFullScreen Screen.fullScreenMode = isFullScreen // 설정을 바꿀거에요 -> 화면 모드를
? FullScreenMode.FullScreenWindow ? FullScreenMode.FullScreenWindow // 조건이 참이면 전체 화면으로
: FullScreenMode.Windowed; : FullScreenMode.Windowed; // 조건이 거짓이면 창 모드로
ApplyResolution(); 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 && if (FilteredResolutions[i].width == Screen.width && // 조건이 맞으면 실행할거에요 -> 너비가 현재 화면과 같고
FilteredResolutions[i].height == Screen.height) FilteredResolutions[i].height == Screen.height) // 높이가 현재 화면과 같다면
{ {
resolutionIndex = i; resolutionIndex = i; // 값을 저장할거에요 -> 해당 인덱스를
return; 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(); // 🔥 씬 로드 직후 다시 적용 // 실행할거에요 -> 설정 적용 함수를
} }
} }

View File

@ -1,42 +1,42 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System; using System; // 기본 시스템 기능을 사용할거에요 -> System을
public class EnemyHealth : MonoBehaviour, IDamageable public class EnemyHealth : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 EnemyHealth를
{ {
[Header("--- 능력치 ---")] [Header("--- 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 능력치 --- 를
[SerializeField] private float maxHealth = 50f; [SerializeField] private float maxHealth = 50f; // 변수를 선언할거에요 -> 최대 체력(50.0)을 maxHealth에
private float _currentHealth; private float _currentHealth; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHealth를
public event Action<float, float> OnHealthChanged; public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 시 현재/최대를 알릴 OnHealthChanged를
public bool IsDead { get; private set; } public bool IsDead { get; private set; } // 프로퍼티를 선언할거에요 -> 사망 여부를 외부에서 읽기만 가능하게 IsDead에
private void Start() private void Start() // 함수를 실행할거에요 -> 시작 시 Start를
{ {
_currentHealth = maxHealth; _currentHealth = maxHealth; // 값을 초기화할거에요 -> 현재 체력을 최대 체력으로
// ⭐ 시작하자마자 UI에 현재 숫자가 뜨도록 신호 발송 // ⭐ 시작하자마자 UI에 현재 숫자가 뜨도록 신호 발송
OnHealthChanged?.Invoke(_currentHealth, maxHealth); OnHealthChanged?.Invoke(_currentHealth, maxHealth); // 이벤트를 실행할거에요 -> 초기 체력 상태를 알리기 위해
} }
// IDamageable 인터페이스 구현 // 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 갱신 신호 발송 // ⭐ 피격 시 로그와 함께 UI 갱신 신호 발송
Debug.Log($"{gameObject.name} 피격! 체력: {{{_currentHealth}/{maxHealth}}}"); Debug.Log($"{gameObject.name} 피격! 체력: {{{_currentHealth}/{maxHealth}}}"); // 로그를 출력할거에요 -> 피격 정보와 남은 체력을
OnHealthChanged?.Invoke(_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; if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽음 처리 중이라면
IsDead = true; IsDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참으로
Debug.Log($"{gameObject.name} 사망!"); Debug.Log($"{gameObject.name} 사망!"); // 로그를 출력할거에요 -> 사망 메시지를
Destroy(gameObject, 1.5f); Destroy(gameObject, 1.5f); // 파괴할거에요 -> 이 오브젝트를 1.5초 뒤에
} }
} }

View File

@ -1,101 +1,100 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] // 컴포넌트를 강제로 추가할거에요 -> MeshFilter와 MeshRenderer를
public class ArrowRangeUI : MonoBehaviour public class ArrowRangeUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 ArrowRangeUI를
{ {
// 🚨 수정됨: 끝부분의 \ 기호를 모두 삭제했습니다. [Header("--- 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 참조 --- 를
[Header("--- 참조 ---")] [SerializeField] private PlayerAttack attackScript; // 변수를 선언할거에요 -> 플레이어 공격 스크립트를 연결할 attackScript를
[SerializeField] private PlayerAttack attackScript; // PlayerAttack 스크립트 연결
[Header("--- UI 설정 ---")] [Header("--- UI 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- UI 설정 --- 을
[Tooltip("사거리 표시기의 색상")] [Tooltip("사거리 표시기의 색상")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private Color uiColor = new Color(1f, 1f, 0f, 0.5f); // 노란색 반투명 [SerializeField] private Color uiColor = new Color(1f, 1f, 0f, 0.5f); // 변수를 선언할거에요 -> UI 색상(노란색 반투명)을 uiColor에
[Tooltip("화살표 UI의 폭 (두께)")] [Tooltip("화살표 UI의 폭 (두께)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private float lineWidth = 1.0f; [SerializeField] private float lineWidth = 1.0f; // 변수를 선언할거에요 -> 선 두께(1.0)를 lineWidth에
[Header("--- 길이 설정 ---")] [Header("--- 길이 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 길이 설정 --- 을
[Tooltip("차징 0%일 때의 최소 길이 (기본 사거리)")] [Tooltip("차징 0%일 때의 최소 길이 (기본 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private float minLength = 5f; [SerializeField] private float minLength = 5f; // 변수를 선언할거에요 -> 최소 길이(5.0)를 minLength에
[Tooltip("풀차징일 때의 최대 길이 (최대 사거리)")] [Tooltip("풀차징일 때의 최대 길이 (최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private float maxLength = 20f; [SerializeField] private float maxLength = 20f; // 변수를 선언할거에요 -> 최대 길이(20.0)를 maxLength에
private Mesh _mesh; private Mesh _mesh; // 변수를 선언할거에요 -> UI를 그릴 메쉬를 _mesh에
private MeshFilter _meshFilter; private MeshFilter _meshFilter; // 변수를 선언할거에요 -> 메쉬 필터 컴포넌트를 _meshFilter에
private MeshRenderer _meshRenderer; private MeshRenderer _meshRenderer; // 변수를 선언할거에요 -> 메쉬 렌더러 컴포넌트를 _meshRenderer에
private void Awake() private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
{ {
// 메쉬 초기화 // 메쉬 초기화
_mesh = new Mesh(); _mesh = new Mesh(); // 생성할거에요 -> 빈 메쉬 객체를
_meshFilter = GetComponent<MeshFilter>(); _meshFilter = GetComponent<MeshFilter>(); // 컴포넌트를 가져올거에요 -> 메쉬 필터를
_meshRenderer = GetComponent<MeshRenderer>(); _meshRenderer = GetComponent<MeshRenderer>(); // 컴포넌트를 가져올거에요 -> 메쉬 렌더러를
_meshFilter.mesh = _mesh; _meshFilter.mesh = _mesh; // 할당할거에요 -> 생성한 메쉬를 필터에
// 재질(Material)이 없다면 임시로 생성 // 재질(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. 공격 스크립트가 없거나 차징 중이 아니면 끄기 // 1. 공격 스크립트가 없거나 차징 중이 아니면 끄기
if (attackScript == null || !attackScript.IsCharging) if (attackScript == null || !attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 없거나 차징 중이 아니라면
{ {
_meshRenderer.enabled = false; _meshRenderer.enabled = false; // 기능을 끌거에요 -> 렌더러를 (안 보이게)
return; return; // 중단할거에요 -> 업데이트를
} }
_meshRenderer.enabled = true; _meshRenderer.enabled = true; // 기능을 켤거에요 -> 렌더러를 (보이게)
// 2. 차징 진행도(0.0 ~ 1.0) 가져오기 // 2. 차징 진행도(0.0 ~ 1.0) 가져오기
float progress = attackScript.ChargeProgress; float progress = attackScript.ChargeProgress; // 값을 가져올거에요 -> 차징 진행률을 progress에
// 3. 진행도에 따라 길이 계산 (Lerp로 부드럽게 늘어남) // 3. 진행도에 따라 길이 계산 (Lerp로 부드럽게 늘어남)
float currentLength = Mathf.Lerp(minLength, maxLength, progress); float currentLength = Mathf.Lerp(minLength, maxLength, progress); // 값을 계산할거에요 -> 최소~최대 사이에서 진행도에 맞는 길이를
// 4. 직사각형 메쉬 그리기 // 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]; Vector3[] vertices = new Vector3[4]; // 배열을 생성할거에요 -> 정점 4개를 담을 vertices를
int[] triangles = new int[6]; int[] triangles = new int[6]; // 배열을 생성할거에요 -> 삼각형 인덱스 6개를 담을 triangles를
Vector2[] uvs = new Vector2[4]; Vector2[] uvs = new Vector2[4]; // 배열을 생성할거에요 -> UV 좌표 4개를 담을 uvs를
float halfWidth = width / 2f; float halfWidth = width / 2f; // 값을 계산할거에요 -> 폭의 절반을 halfWidth에
// 정점 4개 정의 (플레이어 발밑 기준 앞으로 뻗어나감) // 정점 4개 정의 (플레이어 발밑 기준 앞으로 뻗어나감)
vertices[0] = new Vector3(-halfWidth, 0.1f, 0); vertices[0] = new Vector3(-halfWidth, 0.1f, 0); // 값을 넣을거에요 -> 왼쪽 아래 정점을 (시작점)
vertices[1] = new Vector3(halfWidth, 0.1f, 0); vertices[1] = new Vector3(halfWidth, 0.1f, 0); // 값을 넣을거에요 -> 오른쪽 아래 정점을 (시작점)
vertices[2] = new Vector3(-halfWidth, 0.1f, length); vertices[2] = new Vector3(-halfWidth, 0.1f, length); // 값을 넣을거에요 -> 왼쪽 위 정점을 (끝점)
vertices[3] = new Vector3(halfWidth, 0.1f, length); vertices[3] = new Vector3(halfWidth, 0.1f, length); // 값을 넣을거에요 -> 오른쪽 위 정점을 (끝점)
// 삼각형 연결 // 삼각형 연결
triangles[0] = 0; triangles[0] = 0; // 값을 넣을거에요 -> 첫 번째 삼각형의 1번 점 인덱스를
triangles[1] = 2; triangles[1] = 2; // 값을 넣을거에요 -> 첫 번째 삼각형의 2번 점 인덱스를
triangles[2] = 1; triangles[2] = 1; // 값을 넣을거에요 -> 첫 번째 삼각형의 3번 점 인덱스를
triangles[3] = 2; triangles[3] = 2; // 값을 넣을거에요 -> 두 번째 삼각형의 1번 점 인덱스를
triangles[4] = 3; triangles[4] = 3; // 값을 넣을거에요 -> 두 번째 삼각형의 2번 점 인덱스를
triangles[5] = 1; triangles[5] = 1; // 값을 넣을거에요 -> 두 번째 삼각형의 3번 점 인덱스를
// UV 매핑 // UV 매핑
uvs[0] = new Vector2(0, 0); uvs[0] = new Vector2(0, 0); // 값을 넣을거에요 -> 0번 정점의 텍스처 좌표를
uvs[1] = new Vector2(1, 0); uvs[1] = new Vector2(1, 0); // 값을 넣을거에요 -> 1번 정점의 텍스처 좌표를
uvs[2] = new Vector2(0, 1); uvs[2] = new Vector2(0, 1); // 값을 넣을거에요 -> 2번 정점의 텍스처 좌표를
uvs[3] = new Vector2(1, 1); uvs[3] = new Vector2(1, 1); // 값을 넣을거에요 -> 3번 정점의 텍스처 좌표를
// 메쉬 적용 // 메쉬 적용
_mesh.Clear(); _mesh.Clear(); // 비울거에요 -> 기존 메쉬 데이터를
_mesh.vertices = vertices; _mesh.vertices = vertices; // 값을 넣을거에요 -> 새로 만든 정점들을
_mesh.triangles = triangles; _mesh.triangles = triangles; // 값을 넣을거에요 -> 새로 만든 삼각형들을
_mesh.uv = uvs; _mesh.uv = uvs; // 값을 넣을거에요 -> 새로 만든 UV를
_mesh.RecalculateNormals(); _mesh.RecalculateNormals(); // 계산할거에요 -> 조명을 위한 법선 벡터를
_mesh.RecalculateBounds(); _mesh.RecalculateBounds(); // 계산할거에요 -> 메쉬의 경계 영역을
} }
} }

View File

@ -1,56 +1,93 @@
using UnityEngine; using UnityEngine; // 유니티 기본 기능(카메라, Mathf 등)을 쓰기 위해 불러올거에요 -> UnityEngine를
using UnityEngine.UI; // ⭐ Image 컴포넌트 사용을 위해 필수! using UnityEngine.UI; // Image 컴포넌트를 쓰기 위해 불러올거에요 -> UnityEngine.UI를
using TMPro; using TMPro; // TextMeshProUGUI를 쓰기 위해 불러올거에요 -> TMPro를
public class HPUibar : MonoBehaviour public class HPUibar : MonoBehaviour // 클래스를 선언할거에요 -> 체력바 UI를 갱신하는 HPUibar를
{ { // 코드 블록을 시작할거에요 -> HPUibar 범위를
[Header("--- 참조 ---")]
[SerializeField] private MonoBehaviour healthSource;
// ⭐ [수정] Slider 대신 Image 타입을 사용합니다. [Header("--- 참조 ---")] // 인스펙터에 제목을 표시할거에요 -> 참조 섹션을
[SerializeField] private Image hpFillImage; [SerializeField] private MonoBehaviour healthSource; // 변수를 선언할거에요 -> 체력 소스(플레이어/몹 등)를 healthSource에
[SerializeField] private TextMeshProUGUI hpText;
private void Start() [Header("--- UI ---")] // 인스펙터에 제목을 표시할거에요 -> UI 섹션을
{ [SerializeField] private Image hpFillImage; // 변수를 선언할거에요 -> 체력 게이지 채움(Image.fillAmount)용 hpFillImage를
// 1. 체력 소스 연결 (플레이어, 더미, 몬스터 등 모두 대응) [SerializeField] private TextMeshProUGUI hpText; // 변수를 선언할거에요 -> 체력 텍스트 표시용 hpText를
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;
// 시작 시 풀피로 설정 private PlayerHealth _playerHealth; // 변수를 선언할거에요 -> PlayerHealth를 캐싱할 _playerHealth를
if (hpFillImage != null) hpFillImage.fillAmount = 1f; private Stats _playerStats; // 변수를 선언할거에요 -> PlayerHealth의 Stats를 캐싱할 _playerStats를
}
private void UpdateUI(float current, float max) private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를
{ { // 코드 블록을 시작할거에요 -> Start 범위를
// 2. ⭐ [핵심 수정] 이미지의 fillAmount를 0 ~ 1 사이 값으로 조절합니다.
if (hpFillImage != null && max > 0)
{
hpFillImage.fillAmount = current / max;
}
// 텍스트 업데이트 // ✅ PlayerHealth는 UnityEvent<float> (ratio만 줌)이므로 AddListener 방식으로 구독할거에요 -> UnityEvent 전용 처리
if (hpText != null) if (healthSource is PlayerHealth ph) // 조건을 검사할거에요 -> healthSource가 PlayerHealth인지
{ { // 코드 블록을 시작할거에요 -> PlayerHealth 처리 범위를
hpText.text = $"{Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)}"; _playerHealth = ph; // 값을 저장할거에요 -> PlayerHealth 캐싱
} _playerStats = ph.GetComponent<Stats>(); // 컴포넌트를 가져올거에요 -> 같은 오브젝트의 Stats를 캐싱
} ph.OnHealthChanged.AddListener(UpdateUIFromRatio); // 이벤트를 구독할거에요 -> UnityEvent<float>라 AddListener 사용
ForceRefreshPlayerUI(); // 시작 시 UI를 강제로 한 번 맞출거에요 -> CurrentHP 기반으로
} // 코드 블록을 끝낼거에요 -> PlayerHealth 처리
private void LateUpdate() // ✅ 나머지는 (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로
if (Camera.main != null) else if (healthSource is MonsterClass mc) mc.OnHealthChanged += UpdateUI; // 몬스터면 (current,max) 이벤트 구독할거에요 -> UpdateUI로
transform.LookAt(transform.position + Camera.main.transform.forward);
}
private void OnDestroy() if (hpFillImage != null) hpFillImage.fillAmount = 1f; // 조건이 맞으면 실행할거에요 -> 시작 시 게이지를 풀로 표시
{ } // 코드 블록을 끝낼거에요 -> Start를
// 이벤트 해제 (메모리 누수 방지)
if (healthSource is PlayerHealth ph) ph.OnHealthChanged -= UpdateUI; private void ForceRefreshPlayerUI() // 함수를 선언할거에요 -> PlayerHealth용 UI 강제 갱신 함수 ForceRefreshPlayerUI를
else if (healthSource is TrainingDummy td) td.OnHealthChanged -= UpdateUI; { // 코드 블록을 시작할거에요 -> ForceRefreshPlayerUI 범위를
else if (healthSource is EnemyHealth eh) eh.OnHealthChanged -= UpdateUI; if (_playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 없으면
else if (healthSource is MonsterClass mc) mc.OnHealthChanged -= UpdateUI; 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<float>)가 보내는 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를

View File

@ -1,43 +1,43 @@
using UnityEngine; using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using TMPro; using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
public class PlayerStatsUI : MonoBehaviour public class PlayerStatsUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerStatsUI를
{ {
[Header("--- 데이터 연결 ---")] [Header("--- 데이터 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 데이터 연결 --- 을
[SerializeField] private Stats playerStats; [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트를
[Header("--- UI 오브젝트 ---")] [Header("--- UI 오브젝트 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- UI 오브젝트 --- 를
[SerializeField] private GameObject statWindowPanel; [SerializeField] private GameObject statWindowPanel; // 변수를 선언할거에요 -> 스탯 창 패널 오브젝트를
[Header("--- 텍스트 UI ---")] [Header("--- 텍스트 UI ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 텍스트 UI --- 를
[SerializeField] private TextMeshProUGUI maxHealthText; [SerializeField] private TextMeshProUGUI maxHealthText; // 변수를 선언할거에요 -> 최대 체력 텍스트 UI를
// [제거] strengthText 변수는 인스펙터에서 비워두거나 삭제하세요. // [제거] strengthText 변수는 인스펙터에서 비워두거나 삭제하세요.
[SerializeField] private TextMeshProUGUI damageText; [SerializeField] private TextMeshProUGUI damageText; // 변수를 선언할거에요 -> 데미지 텍스트 UI를
[SerializeField] private TextMeshProUGUI speedText; [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; if (statWindowPanel == null) return; // 조건이 맞으면 중단할거에요 -> 패널이 연결되지 않았다면
bool isActive = !statWindowPanel.activeSelf; bool isActive = !statWindowPanel.activeSelf; // 값을 반전시킬거에요 -> 현재 활성화 상태를 뒤집어서
statWindowPanel.SetActive(isActive); statWindowPanel.SetActive(isActive); // 설정을 바꿀거에요 -> 패널의 활성화 상태를
if (isActive) UpdateStatTexts(); 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자리)
} }
} }