From 2a4c20435fbbc26313e2e62a09d3a4ccbb077e12 Mon Sep 17 00:00:00 2001 From: hydrozen1178 Date: Fri, 13 Feb 2026 00:23:25 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EC=8A=A4=20=EB=AA=AC=EC=8A=A4?= =?UTF-8?q?=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .plastic/plastic.changes | Bin 0 -> 2086 bytes .plastic/plastic.wktree | Bin 1768496 -> 1768920 bytes Assets/0.SCENE/MainGame.unity | 235 ++++++- .../MyMonster/BossMonsterAnimater.controller | 8 +- Assets/1.myPrefab/Player.prefab | 45 +- Assets/3D Assets/BossKK.fbx | Bin 0 -> 422044 bytes Assets/3D Assets/BossKK.fbx.meta | 109 +++ .../BossAnimation'/BossIdle.fbx.meta | 432 +++++++++++- .../BossAnimation'/BossKK.controller | 95 +++ .../BossAnimation'/BossKK.controller.meta | 8 + .../BossAnimation'/BossWalk.fbx.meta | 432 +++++++++++- Assets/Scripts/Camera/Effects/CamShake.cs | 134 ++-- Assets/Scripts/Camera/PlayerAim.cs | 30 +- .../Card_Scripts/RandomStatCardInstance.cs | 98 +-- Assets/Scripts/Combat/Components/Health.cs | 205 +++--- Assets/Scripts/Combat/Debug/DamageBot.cs | 36 +- .../Scripts/Combat/Interfaces/IDamageable.cs | 14 +- Assets/Scripts/Combat/WePonHitBox.cs | 72 +- Assets/Scripts/Combat/WeponConfig.cs | 66 +- Assets/Scripts/Enemy/AI/ChargeMonster.cs | 236 +++---- Assets/Scripts/Enemy/AI/ExplodeMonster.cs | 208 +++--- Assets/Scripts/Enemy/AI/Monster Weapon.cs | 137 ++-- Assets/Scripts/Enemy/AI/MonsterClass.cs | 403 ++++++----- Assets/Scripts/Enemy/AI/NormalMonster.cs | 195 +++--- Assets/Scripts/Enemy/AI/Range Monster.cs | 247 ++++--- Assets/Scripts/Enemy/AI/Rangedbase.cs | 86 +-- .../Scripts/Enemy/BossAI/BossCounterConfig.cs | 92 ++- .../Enemy/BossAI/BossCounterFeedback.cs | 164 +++-- .../Enemy/BossAI/BossCounterPersistence.cs | 138 ++-- .../Scripts/Enemy/BossAI/BossCounterSystem.cs | 339 +++++---- Assets/Scripts/Enemy/BossAI/BossMonster.cs | 663 +++++++++--------- .../Scripts/Enemy/BossAI/BossZoneTrigger.cs | 32 +- Assets/Scripts/Enemy/BossAI/CounterType.cs | 20 +- .../Scripts/Enemy/Spawner/Monster Spwaner.cs | 122 ++-- .../Enemy/Spawner/MonsterUpdateManager.cs | 22 +- Assets/Scripts/Enemy/Types/DummyBot.cs | 56 +- Assets/Scripts/Items/Pickups/HealthPotion.cs | 30 +- Assets/Scripts/Items/VFX/ItemHighlight.cs | 108 +-- Assets/Scripts/Obsession/BossPatternPhases.cs | 212 +++--- Assets/Scripts/Obsession/BossRoomTrigger.cs | 37 +- Assets/Scripts/Obsession/ObsessionSystem.cs | 190 +++-- Assets/Scripts/Obsession/RoomClear.cs | 30 +- Assets/Scripts/Player/Animation.cs | 34 +- Assets/Scripts/Player/Combat/Arrow.cs | 268 +++---- Assets/Scripts/Player/Combat/ArrowData.cs | 54 +- Assets/Scripts/Player/Combat/Attack.cs | 392 +++++------ .../Scripts/Player/Combat/StatusEffectType.cs | 1 + .../Scripts/Player/Controller/ArrowPickup.cs | 393 +++++------ .../Controller/PlayerBehaviorTracker.cs | 140 ++-- .../Scripts/Player/Controller/PlayerInput.cs | 50 +- .../Player/Controller/PlayerMovement.cs | 281 +++----- Assets/Scripts/Player/Effect/Player Effect.cs | 34 +- Assets/Scripts/Player/Equipment/EquipItem.cs | 170 ++--- .../Player/Interaction/Heal/HealthAltar.cs | 48 +- .../Player/Interaction/PlayerInteraction.cs | 88 +-- .../Scripts/Player/Stats/PlayerLevelSystem.cs | 84 +-- Assets/Scripts/Player/Stats/StatType.cs | 15 +- Assets/Scripts/Player/Stats/Stats.cs | 66 +- .../Scripts/Player/Upgrade/Data/CardData.cs | 28 +- .../Player/Upgrade/Data/RandomStatCardData.cs | 32 +- Assets/Scripts/Player/Upgrade/UI/CardUI.cs | 70 +- .../System_Scripts/SettingPanelController.cs | 84 +-- .../Systems/ObjectPool/GenericObjectPool.cs | 58 +- .../Editor/PlayerRangeManagerEditor.cs | 76 +- .../Optimization/Player Render Manager.cs | 79 +-- .../Optimization/PlayerRangeManager.cs | 198 +++--- .../Systems/Optimization/RenderGroup.cs | 160 ++--- .../Optimization/RenderOptimizationConfig.cs | 115 ++- Assets/Scripts/Systems/Scene/LoadScene.cs | 20 +- .../Settings/GameStopPanelController.cs | 60 +- .../Systems/Settings/GraphicSettingUI.cs | 53 +- .../Systems/Settings/SettingResolutionUI.cs | 65 +- .../Systems/Settings/SettingsManager.cs | 152 ++-- Assets/Scripts/UI/Enemy/EnemyHP Ui.cs | 44 +- Assets/Scripts/UI/HUD/CrossHairUI.cs | 119 ++-- Assets/Scripts/UI/HUD/HP Ui bar.cs | 133 ++-- Assets/Scripts/UI/HUD/Player Stat UI.cs | 46 +- 77 files changed, 5371 insertions(+), 4125 deletions(-) create mode 100644 .plastic/plastic.changes create mode 100644 Assets/3D Assets/BossKK.fbx create mode 100644 Assets/3D Assets/BossKK.fbx.meta create mode 100644 Assets/5.Enemy Animation/BossAnimation'/BossKK.controller create mode 100644 Assets/5.Enemy Animation/BossAnimation'/BossKK.controller.meta diff --git a/.plastic/plastic.changes b/.plastic/plastic.changes new file mode 100644 index 0000000000000000000000000000000000000000..e6e6549b86f23c4489a554e281dab47c22de2c62 GIT binary patch literal 2086 zcmbVNO>EOh7@e^EwTL1vkjtX1K!CQZv`(5P9Evt^O=Q1jG8??*o;y4l@Pzids}= zt}a(#Iq4;^_2TdK>$_o3rC_s3z|UZGq)5+)uAd#i;qVFi9fBTJa{7EDqCv4%s#i*M zrfauz{v7`Sj{l zMBwhB!PWrqgzqdJbqHj$$Z>{cLQRuQLLkt0pfkn#e{d#fV7S4o*9+j60f=*V95pnK zjQ|aYJBs$+J@x;f^}{L(TK!wPKKJSRWSp)yFm!OH&}lG?tU?%BO0^2ve6mo@AzzQx zbt1YL8ZIcL$YWnYHGF<11@)FVs=BP!lWE3=5hlf1ixEE7;O2%L+GBkKYe7Vwg!Ptamw6stRT26t@$;@#U`OlaLhUZ$v`d zj%!sqs@6nCq4I{v!KDt98`Bb;XeZb%3yr3=TMKCMvky2nt{eP8fD@evTw~!}#LQkW z;x?ONP%QOFgNce@V`g=Rq|{? zUr}T8_RvSrQ$0=wrdUBWmIPf2w+z+lVs!M=Q=j5L`hq6LyFt(s=UITwaIj>(P)0TekQYu*t`(QAe{jJY;*^pSmrQNH%1Ukjuu#*g z)DjDQ{h;u4TU)s28BU@(B>ASy1XASG|%GG=$v4 zgA4tOa}py%5)EB4U4pW596drEZEfQlMWFsYh32dXT+Z4JcNQT-nV=yw-O-;>rny*k zd$B6x9b=9ahh#E9(KdZeB%|W?ib%#Fd#3Xt)6dLcl;eU2)0^pEW-#h_g8~?+l!4(t z5P%W{Yes5fN->BB0t^fn(45Vr0r3w;;`uf`dnTifGm3$XK(#EniJ5t&W|5HSOEz-x z$}q`wHjl`#^e{|G3`$M4tN^AYO=u`FFq{sVerqP9Ts_>Sd*I$)kiEqz2B!-&tlYJ0eNZq>RF6(OqW8Yp9a$0aJMl9PyaHDQ5&K} zc{bxp-EShF3&44XkYgDb&V)?=Ih#?=3U1n7QhmBTa1NuUm>?vs{{M#+46@S$xtPS6 z824>ovWfAH00&Hd`i#Sj3QUX#x9>X4I87Nto85HBRgB`CjPIcZ%l5|Sj46r;-ETRV z6qp%T1Wy;>WU^os+V088#BWj0<&JeirY}%F5jKHRCj@Nwb7T?{7eF&A7aENJr*}p$ zSu#pYKODhy9}yg@B0<4%CTRQFNT!D@7@@gp`ohDE;>?T}g0|m`W9ksE7le8uIX|zY zC_g7BwFt=@XjXqF#}|n362q`cFE~+;blxf%nZaVK+FonY(UHo#2i4( z3B+7L%nigmK+Frod_c?(!~#Gp2*g4_EDXdVKr9NxVn8eo#1cR(xjnp0>Sikd&A&@1 delta 1176 zcma)*ZAep57{~89n|il&ZoX`DRJe4#I3;so`eB8pPAOk@jpdanbX#T4O{S&fmq;S( zlQQV&j3_k;v+yOj*(*82jJyO^h28f&I$t<9Zhu@>)3XW1IpDX*!i)Sg%5v6{^hyb7*&8UAs_TDqBh zoj{u89^pN;Mh}7xVV&=yMqzgU=6GRdK6}u#bAMTO^+{WaJ3r5EudJ>IVR%S5CK8qj z!rqW@^&UatXo~g&3VZWHzO`{wr5l%<(`TOWa+kZvkz-^FEuI2<;fVu#D+;VOC&-)5 z#tHiSB6$TbH}r+%YxE$q^*Di)l%nX?1_Re%K<`cT&AE3=na*jN46;biHM5A>gXbAC z*@-riC(Do4YzFevJqC=n)BIo6^PQrjg=%S13uT31!$ zw*P#_FEqoEeKeQJaG&8@mGlo8veYeXU~>QeX6FuHM(fq8N20tN_`Gdg-w2vl(&NHs(iuRBn7Y8-3!qC$ zss+r;SB}AMk~4rdajlLJ$xNF$-nNH~8bo|URzi3$dUf;I`)e+0K_&dZuHAOWO67!VFb q05l*29LX1CSao=E%IURQUdnU}pg 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�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

QbrE!Vws}&TwB31`$%% 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

_fJ?Q*#hSFZ0tvb_AuftCa_TzV-G+^GH4&q;_= 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 J-^lTO{vVSmTM+;N literal 0 HcmV?d00001 diff --git a/Assets/3D Assets/BossKK.fbx.meta b/Assets/3D Assets/BossKK.fbx.meta new file mode 100644 index 00000000..0bc2decf --- /dev/null +++ b/Assets/3D Assets/BossKK.fbx.meta @@ -0,0 +1,109 @@ +fileFormatVersion: 2 +guid: 930e80507da91c14789fbf72fea30274 +ModelImporter: + serializedVersion: 22200 + internalIDToNameTable: [] + externalObjects: {} + materials: + materialImportMode: 2 + materialName: 0 + materialSearch: 1 + materialLocation: 1 + animations: + legacyGenerateAnimations: 4 + bakeSimulation: 0 + resampleCurves: 1 + optimizeGameObjects: 0 + removeConstantScaleCurves: 0 + motionNodeName: + rigImportErrors: + rigImportWarnings: + animationImportErrors: + animationImportWarnings: + animationRetargetingWarnings: + animationDoRetargetingWarnings: 0 + importAnimatedCustomProperties: 0 + importConstraints: 0 + animationCompression: 3 + animationRotationError: 0.5 + animationPositionError: 0.5 + animationScaleError: 0.5 + animationWrapMode: 0 + extraExposedTransformPaths: [] + extraUserProperties: [] + clipAnimations: [] + isReadable: 0 + meshes: + lODScreenPercentages: [] + globalScale: 1 + meshCompression: 0 + addColliders: 0 + useSRGBMaterialColor: 1 + sortHierarchyByName: 1 + importPhysicalCameras: 1 + importVisibility: 1 + importBlendShapes: 1 + importCameras: 1 + importLights: 1 + nodeNameCollisionStrategy: 1 + fileIdsGeneration: 2 + swapUVChannels: 0 + generateSecondaryUV: 0 + useFileUnits: 1 + keepQuads: 0 + weldVertices: 1 + bakeAxisConversion: 0 + preserveHierarchy: 0 + skinWeightsMode: 0 + maxBonesPerVertex: 4 + minBoneWeight: 0.001 + optimizeBones: 1 + meshOptimizationFlags: -1 + indexFormat: 0 + secondaryUVAngleDistortion: 8 + secondaryUVAreaDistortion: 15.000001 + secondaryUVHardAngle: 88 + secondaryUVMarginMethod: 1 + secondaryUVMinLightmapResolution: 40 + secondaryUVMinObjectScale: 1 + secondaryUVPackMargin: 4 + useFileScale: 1 + strictVertexDataChecks: 0 + tangentSpace: + normalSmoothAngle: 60 + normalImportMode: 0 + tangentImportMode: 3 + normalCalculationMode: 4 + legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0 + blendShapeNormalImportMode: 1 + normalSmoothingSource: 0 + referencedClips: [] + importAnimation: 1 + humanDescription: + serializedVersion: 3 + human: [] + skeleton: [] + armTwist: 0.5 + foreArmTwist: 0.5 + upperLegTwist: 0.5 + legTwist: 0.5 + armStretch: 0.05 + legStretch: 0.05 + feetSpacing: 0 + globalScale: 1 + rootMotionBoneName: + hasTranslationDoF: 0 + hasExtraRoot: 1 + skeletonHasParents: 1 + lastHumanDescriptionAvatarSource: {instanceID: 0} + autoGenerateAvatarMappingIfUnspecified: 1 + animationType: 3 + humanoidOversampling: 1 + avatarSetup: 1 + addHumanoidExtraRootOnlyWhenUsingAvatar: 1 + importBlendShapeDeformPercent: 1 + remapMaterialsIfMaterialImportModeIsNone: 0 + additionalBone: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/5.Enemy Animation/BossAnimation'/BossIdle.fbx.meta b/Assets/5.Enemy Animation/BossAnimation'/BossIdle.fbx.meta index a0c8ea9a..128ceeb3 100644 --- a/Assets/5.Enemy Animation/BossAnimation'/BossIdle.fbx.meta +++ b/Assets/5.Enemy Animation/BossAnimation'/BossIdle.fbx.meta @@ -16,8 +16,7 @@ ModelImporter: optimizeGameObjects: 0 removeConstantScaleCurves: 0 motionNodeName: - rigImportErrors: "Copied Avatar Rig Configuration mis-match. Transform hierarchy - does not match:\n\tTransform 'spine' for human bone 'Hips' not found\n" + rigImportErrors: rigImportWarnings: animationImportErrors: animationImportWarnings: @@ -32,7 +31,36 @@ ModelImporter: animationWrapMode: 0 extraExposedTransformPaths: [] extraUserProperties: [] - clipAnimations: [] + clipAnimations: + - serializedVersion: 16 + name: mixamo.com + takeName: mixamo.com + internalID: -203655887218126122 + firstFrame: 0 + lastFrame: 241 + wrapMode: 0 + orientationOffsetY: 0 + level: 0 + cycleOffset: 0 + loop: 0 + hasAdditiveReferencePose: 0 + loopTime: 1 + loopBlend: 0 + loopBlendOrientation: 0 + loopBlendPositionY: 0 + loopBlendPositionXZ: 0 + keepOriginalOrientation: 0 + keepOriginalPositionY: 1 + keepOriginalPositionXZ: 0 + heightFromFeet: 0 + mirror: 0 + bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000 + curves: [] + events: [] + transformMask: [] + maskType: 3 + maskSource: {instanceID: 0} + additiveReferencePoseFrame: 0 isReadable: 0 meshes: lODScreenPercentages: [] @@ -82,8 +110,402 @@ ModelImporter: importAnimation: 1 humanDescription: serializedVersion: 3 - human: [] - skeleton: [] + human: + - boneName: mixamorig:Hips + humanName: Hips + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftUpLeg + humanName: LeftUpperLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightUpLeg + humanName: RightUpperLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftLeg + humanName: LeftLowerLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightLeg + humanName: RightLowerLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftFoot + humanName: LeftFoot + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightFoot + humanName: RightFoot + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine + humanName: Spine + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine1 + humanName: Chest + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Neck + humanName: Neck + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Head + humanName: Head + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftShoulder + humanName: LeftShoulder + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightShoulder + humanName: RightShoulder + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftArm + humanName: LeftUpperArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightArm + humanName: RightUpperArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftForeArm + humanName: LeftLowerArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightForeArm + humanName: RightLowerArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHand + humanName: LeftHand + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHand + humanName: RightHand + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftToeBase + humanName: LeftToes + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightToeBase + humanName: RightToes + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex1 + humanName: Left Index Proximal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex2 + humanName: Left Index Intermediate + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex3 + humanName: Left Index Distal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex1 + humanName: Right Index Proximal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex2 + humanName: Right Index Intermediate + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex3 + humanName: Right Index Distal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine2 + humanName: UpperChest + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + skeleton: + - name: BossIdle(Clone) + parentName: + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Hips + parentName: BossIdle(Clone) + position: {x: 0.0026833098, y: 0.9943079, z: -0.012676053} + rotation: {x: -0.002174467, y: -0.0005086692, z: 0.0057661077, w: 0.9999809} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine + parentName: mixamorig:Hips + position: {x: -0, y: 0.1171, z: 0.0029} + rotation: {x: 0.1565307, y: -0.00068456325, z: 0.000030100546, w: 0.98767287} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine1 + parentName: mixamorig:Spine + position: {x: -0, y: 0.1367, z: 0} + rotation: {x: 0.09012192, y: -0.0022969642, z: -0.007762773, w: 0.9958978} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine2 + parentName: mixamorig:Spine1 + position: {x: -0, y: 0.1562, z: 0} + rotation: {x: -0.0013413654, y: -0.0019386135, z: -0.0049632005, w: 0.9999849} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Neck + parentName: mixamorig:Spine2 + position: {x: -0, y: 0.1758, z: 0} + rotation: {x: -0.37416202, y: 0.0052613267, z: 0.0064393664, w: 0.927326} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Head + parentName: mixamorig:Neck + position: {x: -0, y: 0.0536, z: 0.0305} + rotation: {x: -0.18870564, y: -0.0236753, z: 0.017435722, w: 0.98159343} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:HeadTop_End + parentName: mixamorig:Head + position: {x: -0, y: 0.1727, z: 0.0982} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftShoulder + parentName: mixamorig:Spine2 + position: {x: -0.064, y: 0.1358, z: -0.0078} + rotation: {x: 0.6866209, y: -0.46074542, z: 0.44875145, w: 0.33895078} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftArm + parentName: mixamorig:LeftShoulder + position: {x: -0, y: 0.1556, z: 0} + rotation: {x: -0.13769072, y: -0.0002792626, z: 0.060832813, w: 0.9886054} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftForeArm + parentName: mixamorig:LeftArm + position: {x: -0, y: 0.2133, z: 0} + rotation: {x: -0.005401833, y: -0.06163869, z: 0.0028284462, w: 0.99807996} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHand + parentName: mixamorig:LeftForeArm + position: {x: -0, y: 0.3139, z: 0} + rotation: {x: -0.06714013, y: 0.43071648, z: -0.18811981, w: 0.8801059} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex1 + parentName: mixamorig:LeftHand + position: {x: -0, y: 0.0657, z: 0} + rotation: {x: 0.05516846, y: -0.65295166, z: -0.05171242, w: 0.75361556} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex2 + parentName: mixamorig:LeftHandIndex1 + position: {x: -0, y: 0.0705, z: 0} + rotation: {x: 0.3534335, y: -0.012413347, z: -0.101619236, w: 0.929841} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex3 + parentName: mixamorig:LeftHandIndex2 + position: {x: -0.0092, y: 0.0512, z: -0.0468} + rotation: {x: -0.13607779, y: 0.1358576, z: 0.17146648, w: 0.9662426} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex4 + parentName: mixamorig:LeftHandIndex3 + position: {x: 0.0269, y: 0.0618, z: -0.0313} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightShoulder + parentName: mixamorig:Spine2 + position: {x: 0.064, y: 0.136, z: 0.0016} + rotation: {x: 0.67744666, y: 0.47600952, z: -0.42826772, w: 0.36203295} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightArm + parentName: mixamorig:RightShoulder + position: {x: -0, y: 0.1546, z: 0} + rotation: {x: -0.13277586, y: -0.21806774, z: -0.04718864, w: 0.9657072} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightForeArm + parentName: mixamorig:RightArm + position: {x: -0, y: 0.2128, z: 0} + rotation: {x: -0.017916152, y: -0.14330766, z: 0.0014052424, w: 0.989515} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHand + parentName: mixamorig:RightForeArm + position: {x: -0, y: 0.3137, z: 0} + rotation: {x: 0.0841442, y: -0.13422275, z: -0.006523322, w: 0.9873507} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex1 + parentName: mixamorig:RightHand + position: {x: -0, y: 0.2098, z: 0} + rotation: {x: 0.06868836, y: 0.15733252, z: -0.014478484, w: 0.98504764} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex2 + parentName: mixamorig:RightHandIndex1 + position: {x: -0, y: 0.0172, z: 0} + rotation: {x: -0.07102833, y: -0.010314832, z: -0.035351228, w: 0.99679434} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex3 + parentName: mixamorig:RightHandIndex2 + position: {x: -0.0018, y: 0.0213, z: 0.0043} + rotation: {x: 0.07075478, y: -0.04816231, z: 0.077679224, w: 0.99329764} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex4 + parentName: mixamorig:RightHandIndex3 + position: {x: 0.0007, y: 0.0188, z: 0.0021} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftUpLeg + parentName: mixamorig:Hips + position: {x: -0.0906, y: -0.0651, z: 0.0026} + rotation: {x: -0.13512881, y: 0.07888633, z: 0.9815783, w: 0.10964156} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftLeg + parentName: mixamorig:LeftUpLeg + position: {x: -0, y: 0.392, z: 0} + rotation: {x: -0.08549402, y: -0.67984086, z: 0.29791063, w: 0.6646476} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftFoot + parentName: mixamorig:LeftLeg + position: {x: -0, y: 0.4038, z: 0} + rotation: {x: 0.37818304, y: 0.57879406, z: -0.44982204, w: 0.5653628} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftToeBase + parentName: mixamorig:LeftFoot + position: {x: -0, y: 0.2346, z: 0} + rotation: {x: 0.3567435, y: -0.07525214, z: 0.12925003, w: 0.92215276} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftToe_End + parentName: mixamorig:LeftToeBase + position: {x: -0, y: 0.1139, z: 0} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightUpLeg + parentName: mixamorig:Hips + position: {x: 0.0906, y: -0.0651, z: 0.0037} + rotation: {x: 0.21856742, y: 0.09107618, z: 0.9678308, w: -0.08507111} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightLeg + parentName: mixamorig:RightUpLeg + position: {x: -0, y: 0.3914, z: 0} + rotation: {x: -0.29833734, y: -0.49995032, z: 0.099081814, w: 0.8069866} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightFoot + parentName: mixamorig:RightLeg + position: {x: -0, y: 0.4064, z: 0} + rotation: {x: 0.4983791, y: 0.46412635, z: -0.31939918, w: 0.6589303} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightToeBase + parentName: mixamorig:RightFoot + position: {x: -0, y: 0.2331, z: 0} + rotation: {x: 0.3513939, y: -0.029390866, z: -0.13634582, w: 0.9257798} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightToe_End + parentName: mixamorig:RightToeBase + position: {x: -0, y: 0.1125, z: 0} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} armTwist: 0.5 foreArmTwist: 0.5 upperLegTwist: 0.5 diff --git a/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller b/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller new file mode 100644 index 00000000..9e9bb287 --- /dev/null +++ b/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller @@ -0,0 +1,95 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1101 &-5377988278109688191 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: [] + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: 0} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 1 + serializedVersion: 3 + m_TransitionDuration: 0.25 + m_TransitionOffset: 0 + m_ExitTime: 0.93775934 + m_HasExitTime: 1 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1107 &-3325615192832722807 +AnimatorStateMachine: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Base Layer + m_ChildStates: + - serializedVersion: 1 + m_State: {fileID: 5540596969216719168} + m_Position: {x: 320, y: 20, z: 0} + m_ChildStateMachines: [] + m_AnyStateTransitions: [] + m_EntryTransitions: [] + m_StateMachineTransitions: {} + m_StateMachineBehaviours: [] + m_AnyStatePosition: {x: 50, y: 20, z: 0} + m_EntryPosition: {x: 50, y: 120, z: 0} + m_ExitPosition: {x: 800, y: 120, z: 0} + m_ParentStateMachinePosition: {x: 800, y: 20, z: 0} + m_DefaultState: {fileID: 5540596969216719168} +--- !u!91 &9100000 +AnimatorController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: BossKK + serializedVersion: 5 + m_AnimatorParameters: [] + m_AnimatorLayers: + - serializedVersion: 5 + m_Name: Base Layer + m_StateMachine: {fileID: -3325615192832722807} + m_Mask: {fileID: 0} + m_Motions: [] + m_Behaviours: [] + m_BlendingMode: 0 + m_SyncedLayerIndex: -1 + m_DefaultWeight: 0 + m_IKPass: 0 + m_SyncedLayerAffectsTiming: 0 + m_Controller: {fileID: 9100000} +--- !u!1102 &5540596969216719168 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: mixamo_com + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: + - {fileID: -5377988278109688191} + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: -203655887218126122, guid: dc29be49c4c60f74899f0410ec955195, type: 3} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: diff --git a/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller.meta b/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller.meta new file mode 100644 index 00000000..a3bc8989 --- /dev/null +++ b/Assets/5.Enemy Animation/BossAnimation'/BossKK.controller.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bff4e8c104f01194fb159c6c309b3a49 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 9100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/5.Enemy Animation/BossAnimation'/BossWalk.fbx.meta b/Assets/5.Enemy Animation/BossAnimation'/BossWalk.fbx.meta index 9e00889a..ed53c3e8 100644 --- a/Assets/5.Enemy Animation/BossAnimation'/BossWalk.fbx.meta +++ b/Assets/5.Enemy Animation/BossAnimation'/BossWalk.fbx.meta @@ -16,8 +16,7 @@ ModelImporter: optimizeGameObjects: 0 removeConstantScaleCurves: 0 motionNodeName: - rigImportErrors: "Copied Avatar Rig Configuration mis-match. Transform hierarchy - does not match:\n\tTransform 'spine' for human bone 'Hips' not found\n" + rigImportErrors: rigImportWarnings: animationImportErrors: animationImportWarnings: @@ -32,7 +31,36 @@ ModelImporter: animationWrapMode: 0 extraExposedTransformPaths: [] extraUserProperties: [] - clipAnimations: [] + clipAnimations: + - serializedVersion: 16 + name: mixamo.com + takeName: mixamo.com + internalID: -203655887218126122 + firstFrame: 0 + lastFrame: 85 + wrapMode: 0 + orientationOffsetY: 0 + level: 0 + cycleOffset: 0 + loop: 0 + hasAdditiveReferencePose: 0 + loopTime: 1 + loopBlend: 0 + loopBlendOrientation: 0 + loopBlendPositionY: 0 + loopBlendPositionXZ: 0 + keepOriginalOrientation: 0 + keepOriginalPositionY: 1 + keepOriginalPositionXZ: 0 + heightFromFeet: 0 + mirror: 0 + bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000 + curves: [] + events: [] + transformMask: [] + maskType: 3 + maskSource: {instanceID: 0} + additiveReferencePoseFrame: 0 isReadable: 0 meshes: lODScreenPercentages: [] @@ -82,8 +110,402 @@ ModelImporter: importAnimation: 1 humanDescription: serializedVersion: 3 - human: [] - skeleton: [] + human: + - boneName: mixamorig:Hips + humanName: Hips + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftUpLeg + humanName: LeftUpperLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightUpLeg + humanName: RightUpperLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftLeg + humanName: LeftLowerLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightLeg + humanName: RightLowerLeg + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftFoot + humanName: LeftFoot + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightFoot + humanName: RightFoot + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine + humanName: Spine + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine1 + humanName: Chest + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Neck + humanName: Neck + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Head + humanName: Head + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftShoulder + humanName: LeftShoulder + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightShoulder + humanName: RightShoulder + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftArm + humanName: LeftUpperArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightArm + humanName: RightUpperArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftForeArm + humanName: LeftLowerArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightForeArm + humanName: RightLowerArm + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHand + humanName: LeftHand + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHand + humanName: RightHand + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftToeBase + humanName: LeftToes + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightToeBase + humanName: RightToes + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex1 + humanName: Left Index Proximal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex2 + humanName: Left Index Intermediate + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:LeftHandIndex3 + humanName: Left Index Distal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex1 + humanName: Right Index Proximal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex2 + humanName: Right Index Intermediate + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:RightHandIndex3 + humanName: Right Index Distal + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + - boneName: mixamorig:Spine2 + humanName: UpperChest + limit: + min: {x: 0, y: 0, z: 0} + max: {x: 0, y: 0, z: 0} + value: {x: 0, y: 0, z: 0} + length: 0 + modified: 0 + skeleton: + - name: BossWalk(Clone) + parentName: + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Hips + parentName: BossWalk(Clone) + position: {x: 0.0025809966, y: 0.9786751, z: -0.010516135} + rotation: {x: -0.06791151, y: 0.046682756, z: -0.025422283, w: 0.9962743} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine + parentName: mixamorig:Hips + position: {x: -0, y: 0.1171, z: 0.0029} + rotation: {x: 0.052984815, y: -0.010016891, z: 0.016819054, w: 0.99840343} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine1 + parentName: mixamorig:Spine + position: {x: -0, y: 0.1367, z: 0} + rotation: {x: 0.08616688, y: -0.018176258, z: 0.023615148, w: 0.99583495} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Spine2 + parentName: mixamorig:Spine1 + position: {x: -0, y: 0.1562, z: 0} + rotation: {x: 0.08652799, y: -0.016902944, z: 0.023167178, w: 0.99583656} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Neck + parentName: mixamorig:Spine2 + position: {x: -0, y: 0.1758, z: 0} + rotation: {x: -0.17461915, y: 0.03961615, z: -0.02823548, w: 0.9834335} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:Head + parentName: mixamorig:Neck + position: {x: -0, y: 0.0536, z: 0.0305} + rotation: {x: -0.18009952, y: 0.088830575, z: 0.009434662, w: 0.97958374} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:HeadTop_End + parentName: mixamorig:Head + position: {x: -0, y: 0.1727, z: 0.0982} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftShoulder + parentName: mixamorig:Spine2 + position: {x: -0.064, y: 0.1358, z: -0.0078} + rotation: {x: 0.54345345, y: -0.44411075, z: 0.5735583, w: 0.4224394} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftArm + parentName: mixamorig:LeftShoulder + position: {x: -0, y: 0.1556, z: 0} + rotation: {x: -0.14027706, y: -0.238704, z: -0.008420287, w: 0.96087044} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftForeArm + parentName: mixamorig:LeftArm + position: {x: -0, y: 0.2133, z: 0} + rotation: {x: -0.016032558, y: 0.018755268, z: 0.011540319, w: 0.999629} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHand + parentName: mixamorig:LeftForeArm + position: {x: -0, y: 0.3139, z: 0} + rotation: {x: 0.016468085, y: 0.11859691, z: -0.113853276, w: 0.98625606} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex1 + parentName: mixamorig:LeftHand + position: {x: -0, y: 0.0657, z: 0} + rotation: {x: 0.043096147, y: -0.6442412, z: -0.05172926, w: 0.76185304} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex2 + parentName: mixamorig:LeftHandIndex1 + position: {x: -0, y: 0.0705, z: 0} + rotation: {x: 0.35360065, y: -0.012077208, z: -0.100781396, w: 0.92987305} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex3 + parentName: mixamorig:LeftHandIndex2 + position: {x: -0.0092, y: 0.0512, z: -0.0468} + rotation: {x: -0.1376931, y: 0.13756303, z: 0.16533016, w: 0.96684176} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftHandIndex4 + parentName: mixamorig:LeftHandIndex3 + position: {x: 0.0269, y: 0.0618, z: -0.0313} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightShoulder + parentName: mixamorig:Spine2 + position: {x: 0.064, y: 0.136, z: 0.0016} + rotation: {x: 0.60436624, y: 0.39707544, z: -0.5637547, w: 0.3990654} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightArm + parentName: mixamorig:RightShoulder + position: {x: -0, y: 0.1546, z: 0} + rotation: {x: -0.12594903, y: 0.034397986, z: -0.037340727, w: 0.9907368} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightForeArm + parentName: mixamorig:RightArm + position: {x: -0, y: 0.2128, z: 0} + rotation: {x: -0.027492605, y: -0.060033023, z: 0.02598758, w: 0.9974792} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHand + parentName: mixamorig:RightForeArm + position: {x: -0, y: 0.3137, z: 0} + rotation: {x: 0.084213026, y: -0.1548261, z: -0.020946275, w: 0.9841231} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex1 + parentName: mixamorig:RightHand + position: {x: -0, y: 0.2098, z: 0} + rotation: {x: 0.064756036, y: -0.025416251, z: -0.020036412, w: 0.9973762} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex2 + parentName: mixamorig:RightHandIndex1 + position: {x: -0, y: 0.0172, z: 0} + rotation: {x: -0.066692896, y: 0.001831721, z: -0.046279315, w: 0.9966981} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex3 + parentName: mixamorig:RightHandIndex2 + position: {x: -0.0018, y: 0.0213, z: 0.0043} + rotation: {x: 0.07979187, y: -0.0087349545, z: 0.05717984, w: 0.9951319} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightHandIndex4 + parentName: mixamorig:RightHandIndex3 + position: {x: 0.0007, y: 0.0188, z: 0.0021} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftUpLeg + parentName: mixamorig:Hips + position: {x: -0.0906, y: -0.0651, z: 0.0026} + rotation: {x: -0.092424944, y: 0.04025192, z: 0.9944845, w: 0.028948884} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftLeg + parentName: mixamorig:LeftUpLeg + position: {x: -0, y: 0.392, z: 0} + rotation: {x: -0.14082664, y: -0.72221404, z: 0.29314968, w: 0.61044085} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftFoot + parentName: mixamorig:LeftLeg + position: {x: -0, y: 0.4038, z: 0} + rotation: {x: 0.3932538, y: 0.6461467, z: -0.377344, w: 0.53428215} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftToeBase + parentName: mixamorig:LeftFoot + position: {x: -0, y: 0.2346, z: 0} + rotation: {x: 0.3386809, y: -0.052146785, z: 0.01728521, w: 0.9392961} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:LeftToe_End + parentName: mixamorig:LeftToeBase + position: {x: -0, y: 0.1139, z: 0} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightUpLeg + parentName: mixamorig:Hips + position: {x: 0.0906, y: -0.0651, z: 0.0037} + rotation: {x: 0.10251077, y: 0.036421414, z: 0.98532605, w: -0.1315201} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightLeg + parentName: mixamorig:RightUpLeg + position: {x: -0, y: 0.3914, z: 0} + rotation: {x: -0.29762018, y: -0.6152928, z: 0.0053073, w: 0.7299377} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightFoot + parentName: mixamorig:RightLeg + position: {x: -0, y: 0.4064, z: 0} + rotation: {x: 0.5381931, y: 0.48825583, z: -0.36212963, w: 0.5837951} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightToeBase + parentName: mixamorig:RightFoot + position: {x: -0, y: 0.2331, z: 0} + rotation: {x: 0.33407575, y: 0.04256158, z: -0.014975957, w: 0.9414657} + scale: {x: 1, y: 1, z: 1} + - name: mixamorig:RightToe_End + parentName: mixamorig:RightToeBase + position: {x: -0, y: 0.1125, z: 0} + rotation: {x: 0, y: -0, z: -0, w: 1} + scale: {x: 1, y: 1, z: 1} armTwist: 0.5 foreArmTwist: 0.5 upperLegTwist: 0.5 diff --git a/Assets/Scripts/Camera/Effects/CamShake.cs b/Assets/Scripts/Camera/Effects/CamShake.cs index d57d2c9b..fdcb0d9f 100644 --- a/Assets/Scripts/Camera/Effects/CamShake.cs +++ b/Assets/Scripts/Camera/Effects/CamShake.cs @@ -1,78 +1,80 @@ -using UnityEngine; -using Cinemachine; -using System.Collections; +using UnityEngine; // 유니티 기본 기능(Time, Vector3, Coroutine 등)을 쓰기 위해 불러올거에요 -> UnityEngine를 +using Cinemachine; // 시네머신 카메라/임펄스 기능을 쓰기 위해 불러올거에요 -> Cinemachine을 +using System.Collections; // 코루틴(IEnumerator)을 쓰기 위해 불러올거에요 -> System.Collections를 -public class CinemachineShake : MonoBehaviour -{ - public static CinemachineShake Instance { get; private set; } +public class CinemachineShake : MonoBehaviour // 클래스를 선언할거에요 -> 카메라 흔들림/줌/히트슬로우를 담당하는 CinemachineShake를 +{ // 코드 블록을 시작할거에요 -> CinemachineShake 범위를 - [Header("--- 컴포넌트 연결 ---")] - private CinemachineImpulseSource _impulseSource; - [SerializeField] private CinemachineVirtualCamera _vCam; + public static CinemachineShake Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스를 외부에서 읽기만 가능하게 - [Header("--- 줌 설정 ---")] - [SerializeField] private float defaultFOV = 60f; - [SerializeField] private float zoomedFOV = 45f; - [SerializeField] private float zoomSpeed = 5f; + [Header("--- 컴포넌트 연결 ---")] // 인스펙터에 제목을 표시할거에요 -> 컴포넌트 연결 섹션을 + private CinemachineImpulseSource _impulseSource; // 변수를 선언할거에요 -> 임펄스(카메라 흔들림) 발사기 컴포넌트를 담을 _impulseSource를 + [SerializeField] private CinemachineVirtualCamera _vCam; // 변수를 선언할거에요 -> 조절할 가상 카메라를 담을 _vCam을 - private Coroutine _zoomCoroutine; + [Header("--- 줌 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> 줌 설정 섹션을 + [SerializeField] private float defaultFOV = 60f; // 변수를 선언할거에요 -> 기본 FOV를 defaultFOV에 + [SerializeField] private float zoomedFOV = 45f; // 변수를 선언할거에요 -> 줌인 FOV를 zoomedFOV에 + [SerializeField] private float zoomSpeed = 5f; // 변수를 선언할거에요 -> 줌 보간 속도를 zoomSpeed에 - private void Awake() - { - Instance = this; - _impulseSource = GetComponent(); - if (_vCam == null) _vCam = GetComponent(); - } + private Coroutine _zoomCoroutine; // 변수를 선언할거에요 -> 현재 실행 중인 줌 코루틴을 담을 _zoomCoroutine을 - // ⭐ 렉 없는 타격감: 시간을 0.1배속으로 늘림 (Hit-Slow) - public void HitSlow(float duration = 0.15f, float timeScale = 0.1f) - { - StartCoroutine(DoHitSlow(duration, timeScale)); - } + private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + Instance = this; // 값을 넣을거에요 -> 싱글톤 인스턴스를 자기 자신으로 설정 + _impulseSource = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 오브젝트에서 임펄스 소스를 찾아 저장 + if (_vCam == null) _vCam = GetComponent(); // 조건이 맞으면 실행할거에요 -> vCam이 비어있으면 내 오브젝트에서 찾아 채우기 + } // 코드 블록을 끝낼거에요 -> Awake를 - private IEnumerator DoHitSlow(float duration, float timeScale) - { - Time.timeScale = timeScale; // 멈추지 않고 아주 느리게 - yield return new WaitForSecondsRealtime(duration); - Time.timeScale = 1.0f; - } + // ⭐ 렉 없는 타격감: 시간을 0.1배속으로 늘림 (Hit-Slow) // 설명을 적을거에요 -> 짧게 슬로모션을 걸어 타격감을 올리는 기능 + public void HitSlow(float duration = 0.15f, float timeScale = 0.1f) // 함수를 선언할거에요 -> duration/timeScale로 히트슬로우를 주는 HitSlow를 + { // 코드 블록을 시작할거에요 -> HitSlow 범위를 + StartCoroutine(DoHitSlow(duration, timeScale)); // 코루틴을 실행할거에요 -> 실제 슬로우 처리를 하는 DoHitSlow를 + } // 코드 블록을 끝낼거에요 -> HitSlow를 - // ⭐ 카메라 킥: 순간적으로 FOV를 확 줄여 충격을 줌 - public void CameraKick(float kickAmount = 8f) - { - if (_vCam == null) return; - _vCam.m_Lens.FieldOfView -= kickAmount; // 순간 줌인 - SetZoom(false); // AnimateZoom을 통해 부드럽게 복구 - } + private IEnumerator DoHitSlow(float duration, float timeScale) // 코루틴 함수를 선언할거에요 -> 히트슬로우를 수행하는 DoHitSlow를 + { // 코드 블록을 시작할거에요 -> DoHitSlow 범위를 + Time.timeScale = timeScale; // 값을 바꿀거에요 -> 전체 게임 시간을 느리게(멈춤은 아님) + yield return new WaitForSecondsRealtime(duration); // 기다릴거에요 -> 실제 시간 기준으로 duration만큼 + Time.timeScale = 1.0f; // 값을 복구할거에요 -> 게임 시간을 정상(1)으로 + } // 코드 블록을 끝낼거에요 -> DoHitSlow를 - public void ShakeAttack() - { - if (_impulseSource == null) return; - _impulseSource.GenerateImpulse(Vector3.down * 1.5f); - } + // ⭐ 카메라 킥: 순간적으로 FOV를 확 줄여 충격을 줌 // 설명을 적을거에요 -> 순간 줌인 후 부드럽게 복구시키는 연출 + public void CameraKick(float kickAmount = 8f) // 함수를 선언할거에요 -> FOV를 순간 감소시키는 CameraKick을 + { // 코드 블록을 시작할거에요 -> CameraKick 범위를 + if (_vCam == null) return; // 조건이 맞으면 종료할거에요 -> 카메라가 없으면 처리 불가 + _vCam.m_Lens.FieldOfView -= kickAmount; // 값을 바꿀거에요 -> FOV를 줄여서 순간 줌인 효과 + SetZoom(false); // 함수를 실행할거에요 -> 기본 FOV로 부드럽게 돌아가도록 줌 애니메이션 시작 + } // 코드 블록을 끝낼거에요 -> CameraKick을 - public void ShakeNoNo() - { - if (_impulseSource == null) return; - _impulseSource.GenerateImpulse(Vector3.right * 0.4f); - } + public void ShakeAttack() // 함수를 선언할거에요 -> 공격용 흔들림을 주는 ShakeAttack을 + { // 코드 블록을 시작할거에요 -> ShakeAttack 범위를 + if (_impulseSource == null) return; // 조건이 맞으면 종료할거에요 -> 임펄스 소스가 없으면 불가 + _impulseSource.GenerateImpulse(Vector3.down * 1.5f); // 임펄스를 발생시킬거에요 -> 아래 방향으로 강하게 흔들기 + } // 코드 블록을 끝낼거에요 -> ShakeAttack을 - public void SetZoom(bool isZooming) - { - if (_vCam == null) return; - if (_zoomCoroutine != null) StopCoroutine(_zoomCoroutine); - float targetFOV = isZooming ? zoomedFOV : defaultFOV; - _zoomCoroutine = StartCoroutine(AnimateZoom(targetFOV)); - } + public void ShakeNoNo() // 함수를 선언할거에요 -> 좌우로 살짝 흔드는 ShakeNoNo를 + { // 코드 블록을 시작할거에요 -> ShakeNoNo 범위를 + if (_impulseSource == null) return; // 조건이 맞으면 종료할거에요 -> 임펄스 소스가 없으면 불가 + _impulseSource.GenerateImpulse(Vector3.right * 0.4f); // 임펄스를 발생시킬거에요 -> 오른쪽 방향으로 약하게 흔들기 + } // 코드 블록을 끝낼거에요 -> ShakeNoNo를 - private IEnumerator AnimateZoom(float target) - { - if (_vCam == null) yield break; - while (Mathf.Abs(_vCam.m_Lens.FieldOfView - target) > 0.1f) - { - _vCam.m_Lens.FieldOfView = Mathf.Lerp(_vCam.m_Lens.FieldOfView, target, Time.deltaTime * zoomSpeed); - yield return null; - } - _vCam.m_Lens.FieldOfView = target; - } -} \ No newline at end of file + public void SetZoom(bool isZooming) // 함수를 선언할거에요 -> 줌인/줌아웃 목표를 설정하는 SetZoom을 + { // 코드 블록을 시작할거에요 -> SetZoom 범위를 + if (_vCam == null) return; // 조건이 맞으면 종료할거에요 -> 카메라가 없으면 불가 + if (_zoomCoroutine != null) StopCoroutine(_zoomCoroutine); // 조건이 맞으면 실행할거에요 -> 기존 줌 코루틴이 있으면 중지해서 중첩 방지 + float targetFOV = isZooming ? zoomedFOV : defaultFOV; // 목표 FOV를 정할거에요 -> 줌이면 zoomedFOV, 아니면 defaultFOV + _zoomCoroutine = StartCoroutine(AnimateZoom(targetFOV)); // 코루틴을 실행할거에요 -> 목표 FOV로 부드럽게 이동하는 AnimateZoom을 + } // 코드 블록을 끝낼거에요 -> SetZoom을 + + private IEnumerator AnimateZoom(float target) // 코루틴 함수를 선언할거에요 -> FOV를 목표값까지 보간하는 AnimateZoom을 + { // 코드 블록을 시작할거에요 -> AnimateZoom 범위를 + if (_vCam == null) yield break; // 조건이 맞으면 중단할거에요 -> 카메라가 없으면 코루틴 종료 + while (Mathf.Abs(_vCam.m_Lens.FieldOfView - target) > 0.1f) // 반복할거에요 -> 현재 FOV가 목표값과 충분히 가까워질 때까지 + { // 코드 블록을 시작할거에요 -> 보간 루프 + _vCam.m_Lens.FieldOfView = Mathf.Lerp(_vCam.m_Lens.FieldOfView, target, Time.deltaTime * zoomSpeed); // 값을 보간할거에요 -> 현재 FOV를 target 쪽으로 부드럽게 + yield return null; // 다음 프레임까지 기다릴거에요 -> 프레임 단위로 갱신 + } // 코드 블록을 끝낼거에요 -> 보간 루프 + _vCam.m_Lens.FieldOfView = target; // 값을 확정할거에요 -> 마지막에 정확히 target으로 맞추기 + } // 코드 블록을 끝낼거에요 -> AnimateZoom을 + +} // 코드 블록을 끝낼거에요 -> CinemachineShake를 \ No newline at end of file diff --git a/Assets/Scripts/Camera/PlayerAim.cs b/Assets/Scripts/Camera/PlayerAim.cs index 99a94151..00797a47 100644 --- a/Assets/Scripts/Camera/PlayerAim.cs +++ b/Assets/Scripts/Camera/PlayerAim.cs @@ -1,35 +1,35 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class PlayerAim : MonoBehaviour +public class PlayerAim : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerAim을 { - [SerializeField] private PlayerHealth health; + [SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 플레이어 체력 스크립트를 - [Header("회전 설정")] - [SerializeField] private float rotationSpeed = 15f; // 감도 조절 - [SerializeField] private float rotationDeadzone = 1.2f; // 근접전 '뒤돌기' 방지 범위 + [Header("회전 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 회전 설정 을 + [SerializeField] private float rotationSpeed = 15f; // 변수를 선언할거에요 -> 회전 속도(감도)를 + [SerializeField] private float rotationDeadzone = 1.2f; // 변수를 선언할거에요 -> 회전 무시 범위(데드존)를 - public void RotateTowardsMouse() + public void RotateTowardsMouse() // 함수를 선언할거에요 -> 마우스 방향으로 회전하는 RotateTowardsMouse를 { - if (health != null && health.IsDead) return; + if (health != null && health.IsDead) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 죽었다면 // 1. 캐릭터의 월드 위치를 화면 상의 2D 좌표로 변환합니다. - Vector3 playerScreenPos = Camera.main.WorldToScreenPoint(transform.position); + Vector3 playerScreenPos = Camera.main.WorldToScreenPoint(transform.position); // 변환할거에요 -> 캐릭터 월드 좌표를 화면 좌표로 // 2. 마우스의 화면 좌표를 가져옵니다. - Vector3 mousePos = Input.mousePosition; + Vector3 mousePos = Input.mousePosition; // 가져올거에요 -> 현재 마우스 위치를 // 3. 화면상에서 플레이어 -> 마우스로 향하는 2D 방향 벡터를 구합니다. - Vector3 dir = mousePos - playerScreenPos; + Vector3 dir = mousePos - playerScreenPos; // 계산할거에요 -> 마우스와 플레이어 사이의 방향 벡터를 // 4. 거리가 너무 가까우면 회전을 무시합니다 (데드존 적용). - if (dir.magnitude < rotationDeadzone * 50f) return; + if (dir.magnitude < rotationDeadzone * 50f) return; // 조건이 맞으면 중단할거에요 -> 마우스가 캐릭터에 너무 가깝다면 // 5. 2D 방향 벡터를 각도(Atan2)로 변환합니다. // 화면의 X는 월드의 X, 화면의 Y는 월드의 Z에 대응합니다 (쿼터뷰/탑다운 기준). - float angle = Mathf.Atan2(dir.x, dir.y) * Mathf.Rad2Deg; + float angle = Mathf.Atan2(dir.x, dir.y) * Mathf.Rad2Deg; // 계산할거에요 -> 벡터를 각도로 변환해서 // 6. 계산된 각도로 캐릭터를 부드럽게 회전시킵니다. - Quaternion targetRotation = Quaternion.Euler(0, angle, 0); - transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); + Quaternion targetRotation = Quaternion.Euler(0, angle, 0); // 생성할거에요 -> 목표 회전값(쿼터니언)을 + transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); // 회전시킬거에요 -> 현재 회전에서 목표 회전으로 부드럽게 } } \ No newline at end of file diff --git a/Assets/Scripts/Card_Scripts/RandomStatCardInstance.cs b/Assets/Scripts/Card_Scripts/RandomStatCardInstance.cs index f83d1c16..78ac3772 100644 --- a/Assets/Scripts/Card_Scripts/RandomStatCardInstance.cs +++ b/Assets/Scripts/Card_Scripts/RandomStatCardInstance.cs @@ -1,58 +1,60 @@ -using UnityEngine; +using UnityEngine; // Random.Range 같은 유니티 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class RandomStatCardInstance -{ - private RandomStatCardData data; - private StatType statType; - private int value; - private bool isConfirmed = false; - private Stats stats; +public class RandomStatCardInstance // 클래스를 선언할거에요 -> 랜덤 스탯 카드 1장의 동작/상태를 담당하는 RandomStatCardInstance를 +{ // 코드 블록을 시작할거에요 -> RandomStatCardInstance 범위를 - public RandomStatCardInstance(RandomStatCardData data, Stats stats) - { - this.data = data; - this.stats = stats; - } + private RandomStatCardData data; // 변수를 선언할거에요 -> 카드 데이터(가능 스탯/범위)를 담을 data를 + private StatType statType; // 변수를 선언할거에요 -> 이번에 뽑힌 스탯 종류를 담을 statType을 + private int value; // 변수를 선언할거에요 -> 이번에 뽑힌 수치를 담을 value를 + private bool isConfirmed = false; // 변수를 선언할거에요 -> 이미 적용 확정했는지 여부를 isConfirmed에(기본 false) + private Stats stats; // 변수를 선언할거에요 -> 실제 스탯을 적용할 대상(Stats)을 stats에 - public void RollPreview() - { - statType = data.possibleStats[Random.Range(0, data.possibleStats.Length)]; - value = Random.Range(data.minValue, data.maxValue + 1); - isConfirmed = false; - } + public RandomStatCardInstance(RandomStatCardData data, Stats stats) // 생성자를 선언할거에요 -> 데이터와 적용 대상을 받아 초기화하는 생성자를 + { // 코드 블록을 시작할거에요 -> 생성자 범위를 + this.data = data; // 값을 넣을거에요 -> 전달받은 data를 멤버 data에 저장 + this.stats = stats; // 값을 넣을거에요 -> 전달받은 stats를 멤버 stats에 저장 + } // 코드 블록을 끝낼거에요 -> 생성자를 - public string GetText() - { - string sign = value >= 0 ? "+" : ""; - return $"{statType} {sign}{value}"; - } + public void RollPreview() // 함수를 선언할거에요 -> 카드 미리보기(랜덤 결과) 뽑는 RollPreview를 + { // 코드 블록을 시작할거에요 -> RollPreview 범위를 + statType = data.possibleStats[Random.Range(0, data.possibleStats.Length)]; // 값을 뽑을거에요 -> 가능한 스탯 중 하나를 랜덤으로 + value = Random.Range(data.minValue, data.maxValue + 1); // 값을 뽑을거에요 -> min~max 범위에서 랜덤 수치를(+1은 int 상한 포함) + isConfirmed = false; // 상태를 바꿀거에요 -> 아직 확정 전(false)로 + } // 코드 블록을 끝낼거에요 -> RollPreview를 - public void Confirm() - { - if (isConfirmed) return; - ApplyStat(); - isConfirmed = true; - } + public string GetText() // 함수를 선언할거에요 -> UI에 보여줄 텍스트를 만드는 GetText를 + { // 코드 블록을 시작할거에요 -> GetText 범위를 + string sign = value >= 0 ? "+" : ""; // 값을 정할거에요 -> 양수면 + 표시, 음수면 빈 문자열로 + return $"{statType} {sign}{value}"; // 문자열을 반환할거에요 -> "스탯 +값" 형태로 + } // 코드 블록을 끝낼거에요 -> GetText를 - private void ApplyStat() - { - if (stats == null) return; + public void Confirm() // 함수를 선언할거에요 -> 선택 확정 시 실제 적용하는 Confirm을 + { // 코드 블록을 시작할거에요 -> Confirm 범위를 + if (isConfirmed) return; // 조건이 맞으면 종료할거에요 -> 이미 확정했으면 중복 적용 방지 + ApplyStat(); // 함수를 실행할거에요 -> 뽑힌 스탯을 실제로 적용 + isConfirmed = true; // 상태를 바꿀거에요 -> 이제 확정 완료(true)로 + } // 코드 블록을 끝낼거에요 -> Confirm을 - switch (statType) - { - case StatType.Health: - stats.AddMaxHealth(value); - break; + private void ApplyStat() // 함수를 선언할거에요 -> 스탯 적용 로직을 처리하는 ApplyStat을 + { // 코드 블록을 시작할거에요 -> ApplyStat 범위를 + if (stats == null) return; // 조건이 맞으면 종료할거에요 -> 적용 대상이 없으면 아무것도 안 함 - case StatType.Speed: - stats.AddMoveSpeed(value); - break; + switch (statType) // 분기할거에요 -> 어떤 스탯인지에 따라 + { // 코드 블록을 시작할거에요 -> switch 범위를 + case StatType.Health: // 조건을 처리할거에요 -> 체력 스탯이면 + stats.AddMaxHealth(value); // 함수를 실행할거에요 -> 최대 체력에 value만큼 더하기 + break; // 분기를 끝낼거에요 -> switch 탈출 - // ✨ [제거] case StatType.Strength 삭제됨 + case StatType.Speed: // 조건을 처리할거에요 -> 이동속도 스탯이면 + stats.AddMoveSpeed(value); // 함수를 실행할거에요 -> 이동속도에 value만큼 더하기 + break; // 분기를 끝낼거에요 -> switch 탈출 - case StatType.Damage: - stats.AddAttackDamage(value); - break; - } - } -} \ No newline at end of file + // ✨ [제거] case StatType.Strength 삭제됨 // 설명을 적을거에요 -> 힘 스탯은 더 이상 처리하지 않음 + + case StatType.Damage: // 조건을 처리할거에요 -> 공격력 스탯이면 + stats.AddAttackDamage(value); // 함수를 실행할거에요 -> 공격 데미지에 value만큼 더하기 + break; // 분기를 끝낼거에요 -> switch 탈출 + } // 코드 블록을 끝낼거에요 -> switch를 + } // 코드 블록을 끝낼거에요 -> ApplyStat을 + +} // 코드 블록을 끝낼거에요 -> RandomStatCardInstance를 \ No newline at end of file diff --git a/Assets/Scripts/Combat/Components/Health.cs b/Assets/Scripts/Combat/Components/Health.cs index 4397f9a5..903204f1 100644 --- a/Assets/Scripts/Combat/Components/Health.cs +++ b/Assets/Scripts/Combat/Components/Health.cs @@ -1,138 +1,135 @@ -using UnityEngine; -using System; -using System.Collections; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.Events; // 유니티 이벤트 기능을 사용할거에요 -> UnityEngine.Events를 +using System; // C# 기본 이벤트(Action)를 사용할거에요 -> System을 +using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를 -public class PlayerHealth : MonoBehaviour, IDamageable +///

+/// 플레이어의 체력, 피격, 무적, 사망을 관리합니다. +/// +public class PlayerHealth : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> PlayerHealth를 { - [Header("=== 참조 ===")] - [SerializeField] private Stats stats; - [SerializeField] private Animator animator; - [SerializeField] private PlayerAttack attackScript; + [Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 참조 === 를 + [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 스탯 스크립트를 playerStats에 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // 🔗 보스 패턴 스크립트 연결 (Inspector에서 할당) - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - [Header("🔗 보스 패턴 연결")] - public BossPatternPhases bossPattern; + [Header("=== 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 설정 === 을 + [SerializeField] private float invincibleDuration = 1.0f; // 변수를 선언할거에요 -> 무적 시간을 invincibleDuration에 + [SerializeField] private string hitAnimationName = "Player_Hit"; // 변수를 선언할거에요 -> 피격 애니메이션 이름을 hitAnimationName에 - public bool IsDead { get; private set; } - public bool isHit { get; private set; } - public bool isInvincible; // 대시 중 무적 플래그 + [Header("=== 유니티 이벤트 (인스펙터 연결용) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 유니티 이벤트 === 를 + public UnityEvent OnDieEvent; // 이벤트를 선언할거에요 -> 사망 시 발생할 UnityEvent인 OnDieEvent를 + public UnityEvent OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 시 발생할 OnHealthChanged를 - public event Action OnHitEvent, OnDead; - public event Action OnHealthChanged; + // ⭐ [이벤트] += 연산자 오류 방지를 위해 event Action 사용 + public event Action OnHitEvent; + public event Action OnHit; + public event Action OnDie; + public event Action OnDead; - private float _currentHealth; + // ⭐ [핵심 수정 1] 외부에서 수정 가능하도록 set 권한 개방 + public float CurrentHP { get; private set; } // (체력은 함부로 바꾸면 안되니 private set 유지) - private IEnumerator Start() + // "set 접근자에 액세스할 수 없습니다" 오류 해결 -> private 제거 + public bool isInvincible { get; set; } = false; + + public bool IsDead { get; private set; } = false; // 사망 여부는 내부에서만 관리 + + private Animator animator; // 변수를 선언할거에요 -> 애니메이터 컴포넌트를 담을 animator를 + + // ⭐ [핵심 수정 2] "보호 수준 때문에 액세스 불가" 오류 해결 -> public으로 변경 + public bool isHit = false; + + private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를 { - yield return null; - - if (stats != null) - { - _currentHealth = stats.MaxHealth; - OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth); - Debug.Log($"[UI Sync] 초기 체력 설정 완료: {_currentHealth}/{stats.MaxHealth}"); - } - if (animator == null) animator = GetComponent(); - if (attackScript == null) attackScript = GetComponent(); + animator = GetComponentInChildren(); // 컴포넌트를 가져올거에요 -> 자식의 애니메이터를 + if (playerStats == null) playerStats = GetComponent(); // 조건이 맞으면 가져올거에요 -> 스탯 컴포넌트가 비어있다면 내 몸에서 } - public void RefreshHealthUI() + private void Start() // 함수를 실행할거에요 -> 시작 Start를 { - if (stats != null) + if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 스탯이 있다면 { - _currentHealth = Mathf.Min(_currentHealth, stats.MaxHealth); - OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth); + CurrentHP = playerStats.MaxHealth; // 값을 초기화할거에요 -> 현재 체력을 최대 체력으로 + } + else // 조건이 틀리면 실행할거에요 -> 스탯이 없다면 + { + CurrentHP = 100f; // 값을 넣을거에요 -> 기본 체력 100으로 + Debug.LogWarning("⚠️ Stats 컴포넌트가 없습니다! 기본 체력 100 적용."); // 경고를 출력할거에요 + } + + RefreshHealthUI(); // 함수를 실행할거에요 -> 초기 체력 UI 갱신을 + } + + public void TakeDamage(float damage) // 함수를 선언할거에요 -> 외부에서 호출할 피격 함수 TakeDamage를 + { + if (IsDead || isInvincible) return; // 조건이 맞으면 중단할거에요 -> 죽었거나 무적이라면 + + // 실제 데미지 적용 + CurrentHP -= damage; // 값을 뺄거에요 -> 체력에서 데미지를 + if (CurrentHP < 0) CurrentHP = 0; // 조건이 맞으면 보정할거에요 -> 체력이 음수가 안 되게 0으로 + + Debug.Log($"💥 피격! 데미지: {damage}, 남은 체력: {CurrentHP}"); // 로그를 출력할거에요 -> 피격 정보를 + + RefreshHealthUI(); // 함수를 실행할거에요 -> 체력바 갱신을 + + if (CurrentHP <= 0) // 조건이 맞으면 실행할거에요 -> 체력이 바닥났다면 + { + Die(); // 함수를 실행할거에요 -> 사망 처리 Die를 + } + else // 조건이 틀리면 실행할거에요 -> 아직 살았다면 + { + StartCoroutine(HitRoutine()); // 코루틴을 실행할거에요 -> 피격 무적/경직 처리를 + + // 모든 이벤트 호출 + OnHit?.Invoke(); + OnHitEvent?.Invoke(); } } - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // ⭐ 데미지를 받는 함수 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - public void TakeDamage(float amount) + private void Die() // 함수를 선언할거에요 -> 사망 처리 Die를 { - // 무적이거나 이미 죽었으면 데미지 무시 - if (isInvincible || IsDead) return; + if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면 - _currentHealth = Mathf.Max(0, _currentHealth - amount); - OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth); + IsDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참으로 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // 🔗 NEW: 보스 패턴 시스템에 "피격당함"을 알림 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - if (bossPattern != null) - { - // BossPatternPhases 스크립트의 OnPlayerHit() 함수 호출 - // 이 함수 안에서 "패턴 진행 중인지" 체크하고 XP 감점 처리 - bossPattern.OnPlayerHit(); - } + OnDieEvent?.Invoke(); // 유니티 이벤트 + OnDie?.Invoke(); // C# 이벤트 + OnDead?.Invoke(); // C# 이벤트 (호환용) - // ⭐ 피격 시 트리거 및 상태 리셋 함수 호출 - OnHit(); - - OnHitEvent?.Invoke(); // 이벤트 발생 - - if (!IsDead) StartHit(); - if (_currentHealth <= 0) Die(); + Debug.Log("💀 플레이어 사망!"); // 로그를 출력할거에요 -> 사망 메시지를 } - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // ⭐ 피격 시 공격 상태 리셋 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - public void OnHit() + public void RefreshHealthUI() // 함수를 선언할거에요 -> UI를 갱신하는 RefreshHealthUI를 { + float ratio = (playerStats != null && playerStats.MaxHealth > 0) ? CurrentHP / playerStats.MaxHealth : 0; // 비율을 계산할거에요 -> 현재 체력 / 최대 체력으로 + OnHealthChanged?.Invoke(ratio); // 이벤트를 실행할거에요 -> UI 슬라이더 값을 업데이트하도록 + } + + private IEnumerator HitRoutine() // 코루틴 함수를 선언할거에요 -> 피격 경직 및 무적 처리 HitRoutine을 + { + isHit = true; // 상태를 바꿀거에요 -> 경직 상태를 참으로 + isInvincible = true; // 상태를 바꿀거에요 -> 무적 상태를 참으로 + if (animator != null) { - // 1. 남아있는 공격 트리거를 강제로 꺼버림 (유령 공격 방지) - animator.ResetTrigger("Attack"); - - // 2. 다른 공격(예: 투척) 트리거가 있다면 그것도 리셋 - animator.ResetTrigger("Throw"); + animator.Play(hitAnimationName, 0, 0f); // 재생할거에요 -> 설정된 피격 애니메이션을 } - // 3. PlayerAttack 스크립트의 공격 중인 상태 플래그도 강제로 꺼줌 - if (attackScript != null) - { - attackScript.IsAttacking = false; - } + yield return new WaitForSeconds(0.3f); // 기다릴거에요 -> 경직 시간(0.3초)만큼 + isHit = false; // 상태를 바꿀거에요 -> 경직 해제 - Debug.Log("[Combat] 피격으로 인해 공격 예약 및 상태가 초기화되었습니다."); + yield return new WaitForSeconds(invincibleDuration - 0.3f); // 기다릴거에요 -> 남은 무적 시간만큼 + isInvincible = false; // 상태를 바꿀거에요 -> 무적 해제 } - private void StartHit() + public void Heal(float amount) // 함수를 선언할거에요 -> 체력을 회복하는 Heal을 { - isHit = true; - // 인스펙터에 적힌 피격 애니메이션 이름(HitAnime) 재생 - if (animator != null) animator.Play("HitAnime", 0, 0f); + if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 죽은 상태라면 - CancelInvoke(nameof(OnHitEnd)); - Invoke(nameof(OnHitEnd), 0.25f); - } + CurrentHP += amount; // 값을 더할거에요 -> 체력에 회복량을 + if (playerStats != null && CurrentHP > playerStats.MaxHealth) // 조건이 맞으면 실행할거에요 -> 최대 체력을 넘었다면 + CurrentHP = playerStats.MaxHealth; // 값을 제한할거에요 -> 최대 체력으로 - public void OnHitEnd() { isHit = false; } - - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // ⭐ 사망 처리 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - private void Die() - { - IsDead = true; - Cursor.visible = true; - Cursor.lockState = CursorLockMode.None; - OnDead?.Invoke(); - - // ★ ★ ★ NEW: 사망 시 임시 XP를 영구 XP로 전환 ★ ★ ★ - if (ObsessionSystem.instance != null) - { - ObsessionSystem.instance.OnDeathConvertXP(); - } - } - - public void Heal(float amount) - { - if (IsDead) return; - _currentHealth = Mathf.Min(_currentHealth + amount, stats.MaxHealth); - OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth); + RefreshHealthUI(); // 함수를 실행할거에요 -> UI 갱신을 + Debug.Log($"💚 체력 회복: {amount}, 현재: {CurrentHP}"); // 로그를 출력할거에요 -> 회복 정보를 } } \ No newline at end of file diff --git a/Assets/Scripts/Combat/Debug/DamageBot.cs b/Assets/Scripts/Combat/Debug/DamageBot.cs index a510ab43..fc912fdc 100644 --- a/Assets/Scripts/Combat/Debug/DamageBot.cs +++ b/Assets/Scripts/Combat/Debug/DamageBot.cs @@ -1,14 +1,22 @@ -using UnityEngine; -public class DamageBot : MonoBehaviour -{ - [SerializeField] private float damageAmount = 10f, damageInterval = 1.0f; - private float timer; - private void OnTriggerStay(Collider other) - { - if (other.TryGetComponent(out var target)) - { - timer += Time.deltaTime; - if (timer >= damageInterval) { target.TakeDamage(damageAmount); timer = 0f; } - } - } -} \ No newline at end of file +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 + +public class DamageBot : MonoBehaviour // 클래스를 선언할거에요 -> 트리거 안에 있는 대상에게 주기적으로 데미지를 주는 DamageBot을 +{ // 코드 블록을 시작할거에요 -> DamageBot 범위를 + + [SerializeField] private float damageAmount = 10f, damageInterval = 1.0f; // 변수를 선언할거에요 -> 데미지량(10)과 데미지 간격(1초)을 + private float timer; // 변수를 선언할거에요 -> 시간 누적용 타이머를 timer에 + + private void OnTriggerStay(Collider other) // 함수를 선언할거에요 -> 트리거 안에 머무는 동안 계속 호출되는 OnTriggerStay를 + { // 코드 블록을 시작할거에요 -> OnTriggerStay 범위를 + if (other.TryGetComponent(out var target)) // 조건을 검사할거에요 -> 대상이 IDamageable을 가지고 있는지 + { // 코드 블록을 시작할거에요 -> 데미지 가능 대상 처리 + timer += Time.deltaTime; // 값을 더할거에요 -> 지난 프레임 시간만큼 타이머 누적 + if (timer >= damageInterval) // 조건을 검사할거에요 -> 누적 시간이 간격을 넘었는지 + { // 코드 블록을 시작할거에요 -> 데미지 적용 처리 + target.TakeDamage(damageAmount); // 함수를 실행할거에요 -> 대상에게 damageAmount만큼 데미지 주기 + timer = 0f; // 값을 초기화할거에요 -> 다음 틱을 위해 타이머를 0으로 + } // 코드 블록을 끝낼거에요 -> 데미지 적용 처리 + } // 코드 블록을 끝낼거에요 -> 데미지 가능 대상 처리 + } // 코드 블록을 끝낼거에요 -> OnTriggerStay를 + +} // 코드 블록을 끝낼거에요 -> DamageBot을 \ No newline at end of file diff --git a/Assets/Scripts/Combat/Interfaces/IDamageable.cs b/Assets/Scripts/Combat/Interfaces/IDamageable.cs index 1aa2caad..322338a1 100644 --- a/Assets/Scripts/Combat/Interfaces/IDamageable.cs +++ b/Assets/Scripts/Combat/Interfaces/IDamageable.cs @@ -1,6 +1,8 @@ -public interface IDamageable -{ - // ޾ ԼԴϴ. - // (damage) ڷ ޽ϴ. - void TakeDamage(float damage); -} \ No newline at end of file +public interface IDamageable // 인터페이스를 선언할거에요 -> 데미지를 받을 수 있는 대상의 규칙(IDamageable)을 +{ // 코드 블록을 시작할거에요 -> IDamageable 범위를 + + // 공격을 받았을 때 실행될 함수입니다. // 설명을 적을거에요 -> 데미지 받을 때 호출되는 함수임을 + // 데미지 양(damage)을 인자로 받습니다. // 설명을 적을거에요 -> 들어오는 데미지 값이 damage라는 걸 + void TakeDamage(float damage); // 함수 원형을 정의할거에요 -> float damage를 받아 데미지 처리하는 TakeDamage를 + +} // 코드 블록을 끝낼거에요 -> IDamageable을 \ No newline at end of file diff --git a/Assets/Scripts/Combat/WePonHitBox.cs b/Assets/Scripts/Combat/WePonHitBox.cs index 07665e1a..c733014c 100644 --- a/Assets/Scripts/Combat/WePonHitBox.cs +++ b/Assets/Scripts/Combat/WePonHitBox.cs @@ -1,41 +1,43 @@ -using UnityEngine; -using System.Collections.Generic; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 +using System.Collections.Generic; // List를 쓰기 위해 불러올거에요 -> System.Collections.Generic을 -public class WeaponHitBox : MonoBehaviour -{ - private float _damage; - private bool _isActive = false; - private List _hitTargets = new List(); +public class WeaponHitBox : MonoBehaviour // 클래스를 선언할거에요 -> 무기 히트박스(트리거)로 타격을 처리하는 WeaponHitBox를 +{ // 코드 블록을 시작할거에요 -> WeaponHitBox 범위를 - public void EnableHitBox(float damage) - { - _damage = damage; - _isActive = true; - _hitTargets.Clear(); - gameObject.SetActive(true); - } + private float _damage; // 변수를 선언할거에요 -> 이번 공격에서 적용할 데미지를 _damage에 + private bool _isActive = false; // 변수를 선언할거에요 -> 히트박스 활성 여부를 _isActive에(기본 false) + private List _hitTargets = new List(); // 리스트를 만들거에요 -> 이미 때린 대상 저장용 _hitTargets를 - public void DisableHitBox() - { - _isActive = false; - gameObject.SetActive(false); - } + public void EnableHitBox(float damage) // 함수를 선언할거에요 -> 히트박스를 켜고 데미지를 세팅하는 EnableHitBox를 + { // 코드 블록을 시작할거에요 -> EnableHitBox 범위를 + _damage = damage; // 값을 넣을거에요 -> 받은 damage를 _damage로 설정 + _isActive = true; // 상태를 바꿀거에요 -> 히트박스를 활성 상태(true)로 + _hitTargets.Clear(); // 리스트를 비울거에요 -> 이전 공격에서 맞춘 기록을 초기화 + gameObject.SetActive(true); // 오브젝트를 켤거에요 -> 히트박스 트리거 오브젝트 활성화 + } // 코드 블록을 끝낼거에요 -> EnableHitBox를 - private void OnTriggerEnter(Collider other) - { - if (!_isActive || other.CompareTag("Player")) return; + public void DisableHitBox() // 함수를 선언할거에요 -> 히트박스를 끄는 DisableHitBox를 + { // 코드 블록을 시작할거에요 -> DisableHitBox 범위를 + _isActive = false; // 상태를 바꿀거에요 -> 히트박스를 비활성(false)로 + gameObject.SetActive(false); // 오브젝트를 끌거에요 -> 히트박스 트리거 오브젝트 비활성화 + } // 코드 블록을 끝낼거에요 -> DisableHitBox를 - // ⭐ [핵심] 몬스터의 감지 영역(Trigger)은 무시하고 '진짜 몸통'만 타격! - if (other.isTrigger) return; + private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 뭔가 들어오면 호출되는 OnTriggerEnter를 + { // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를 + if (!_isActive || other.CompareTag("Player")) return; // 조건이 맞으면 종료할거에요 -> 비활성이거나 플레이어면 무시 - if (other.TryGetComponent(out var target)) - { - if (!_hitTargets.Contains(target)) - { - target.TakeDamage(_damage); - _hitTargets.Add(target); - Debug.Log($"[Hit] {other.name}의 몸통을 정확히 타격!"); - } - } - } -} \ No newline at end of file + // ⭐ [핵심] 몬스터의 감지 영역(Trigger)은 무시하고 '진짜 몸통'만 타격! // 설명을 적을거에요 -> 트리거 콜라이더는 공격 대상으로 안 잡겠다는 뜻 + if (other.isTrigger) return; // 조건이 맞으면 종료할거에요 -> 상대 콜라이더가 트리거면 무시 + + if (other.TryGetComponent(out var target)) // 조건을 검사할거에요 -> IDamageable을 가진 대상인지 + { // 코드 블록을 시작할거에요 -> 데미지 처리 + if (!_hitTargets.Contains(target)) // 조건을 검사할거에요 -> 이번 공격에서 아직 안 때린 대상인지 + { // 코드 블록을 시작할거에요 -> 1회 타격 처리 + target.TakeDamage(_damage); // 함수를 실행할거에요 -> 대상에게 _damage만큼 데미지 주기 + _hitTargets.Add(target); // 리스트에 추가할거에요 -> 중복 타격 방지를 위해 대상 기록 + Debug.Log($"[Hit] {other.name}의 몸통을 정확히 타격!"); // 로그를 찍을거에요 -> 타격 확인 + } // 코드 블록을 끝낼거에요 -> 1회 타격 처리 + } // 코드 블록을 끝낼거에요 -> 데미지 처리 + } // 코드 블록을 끝낼거에요 -> OnTriggerEnter를 + +} // 코드 블록을 끝낼거에요 -> WeaponHitBox를 \ No newline at end of file diff --git a/Assets/Scripts/Combat/WeponConfig.cs b/Assets/Scripts/Combat/WeponConfig.cs index cdd0949d..d5533ba1 100644 --- a/Assets/Scripts/Combat/WeponConfig.cs +++ b/Assets/Scripts/Combat/WeponConfig.cs @@ -1,39 +1,41 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능 + ScriptableObject를 쓰기 위해 불러올거에요 -> UnityEngine를 -[CreateAssetMenu(fileName = "NewWeapon", menuName = "Weapons/Config")] -public class WeaponConfig : ScriptableObject -{ - [Header("기본 능력치")] - [SerializeField] private float baseDamage = 10f; - [SerializeField] private int requiredStrength = 5; +[CreateAssetMenu(fileName = "NewWeapon", menuName = "Weapons/Config")] // 에셋 생성 메뉴를 추가할거에요 -> Create > Weapons > Config로 WeaponConfig를 만들 수 있게 +public class WeaponConfig : ScriptableObject // 클래스를 선언할거에요 -> 무기 설정 데이터를 담는 ScriptableObject인 WeaponConfig를 +{ // 코드 블록을 시작할거에요 -> WeaponConfig 범위를 - public float BaseDamage => baseDamage; - public int RequiredStrength => requiredStrength; + [Header("기본 능력치")] // 인스펙터에 제목을 표시할거에요 -> 기본 능력치 섹션을 + [SerializeField] private float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 데미지를 10으로 baseDamage에 + [SerializeField] private int requiredStrength = 5; // 변수를 선언할거에요 -> 요구 힘 스탯을 5로 requiredStrength에 - [Header("애니메이션 설정")] - [SerializeField] private string normalAttackTrigger = "Attack"; - [SerializeField] private string chargingBool = "IsCharging"; - [SerializeField] private string throwTrigger = "Throw"; + public float BaseDamage => baseDamage; // 프로퍼티를 선언할거에요 -> baseDamage를 읽기 전용으로 노출 + public int RequiredStrength => requiredStrength; // 프로퍼티를 선언할거에요 -> requiredStrength를 읽기 전용으로 노출 - public string NormalAttackTrigger => normalAttackTrigger; - public string ChargingBool => chargingBool; - public string ThrowTrigger => throwTrigger; + [Header("애니메이션 설정")] // 인스펙터에 제목을 표시할거에요 -> 애니메이션 설정 섹션을 + [SerializeField] private string normalAttackTrigger = "Attack"; // 변수를 선언할거에요 -> 일반 공격 트리거 이름을 normalAttackTrigger에 + [SerializeField] private string chargingBool = "IsCharging"; // 변수를 선언할거에요 -> 차징 여부 bool 파라미터 이름을 chargingBool에 + [SerializeField] private string throwTrigger = "Throw"; // 변수를 선언할거에요 -> 던지기 트리거 이름을 throwTrigger에 - [Header("차징 단계별 설정 (인스펙터 수정)")] - [SerializeField] private float forceLv1 = 10f; - [SerializeField] private float spreadLv1 = 25f; - [SerializeField] private float forceLv2 = 18f; - [SerializeField] private float spreadLv2 = 8f; - [SerializeField] private float forceLv3 = 28f; - // Lv3 Spread는 인스펙터에서 보이지 않게 하거나, 로직에서 무시하도록 처리합니다. + public string NormalAttackTrigger => normalAttackTrigger; // 프로퍼티를 선언할거에요 -> 일반 공격 트리거 문자열을 읽기 전용으로 노출 + public string ChargingBool => chargingBool; // 프로퍼티를 선언할거에요 -> 차징 bool 문자열을 읽기 전용으로 노출 + public string ThrowTrigger => throwTrigger; // 프로퍼티를 선언할거에요 -> 던지기 트리거 문자열을 읽기 전용으로 노출 - // ⭐ 힘(Force)은 단계별로 가져옵니다. - public float GetForce(int lv) => lv == 3 ? forceLv3 : (lv == 2 ? forceLv2 : forceLv1); + [Header("차징 단계별 설정 (인스펙터 수정)")] // 인스펙터에 제목을 표시할거에요 -> 차징 단계별 설정 섹션을 + [SerializeField] private float forceLv1 = 10f; // 변수를 선언할거에요 -> 1단계 힘(발사력)을 10으로 forceLv1에 + [SerializeField] private float spreadLv1 = 25f; // 변수를 선언할거에요 -> 1단계 퍼짐(정확도)을 25로 spreadLv1에 + [SerializeField] private float forceLv2 = 18f; // 변수를 선언할거에요 -> 2단계 힘(발사력)을 18로 forceLv2에 + [SerializeField] private float spreadLv2 = 8f; // 변수를 선언할거에요 -> 2단계 퍼짐(정확도)을 8로 spreadLv2에 + [SerializeField] private float forceLv3 = 28f; // 변수를 선언할거에요 -> 3단계 힘(발사력)을 28로 forceLv3에 + // Lv3 Spread는 인스펙터에서 보이지 않게 하거나, 로직에서 무시하도록 처리합니다. // 설명을 적을거에요 -> 3단계는 퍼짐을 안 쓰는 설계임을 - // ⭐ [수정] 정확도(Spread) 로직: 3단계는 무조건 0(직선) 반환! - public float GetSpread(int lv) - { - if (lv == 3) return 0f; // 풀차징은 무조건 일자로 날아감 - return (lv == 2) ? spreadLv2 : spreadLv1; - } -} \ No newline at end of file + // ⭐ 힘(Force)은 단계별로 가져옵니다. // 설명을 적을거에요 -> 단계에 맞는 force를 반환함을 + public float GetForce(int lv) => lv == 3 ? forceLv3 : (lv == 2 ? forceLv2 : forceLv1); // 값을 반환할거에요 -> lv에 따라 forceLv1/2/3 중 하나를 + + // ⭐ [수정] 정확도(Spread) 로직: 3단계는 무조건 0(직선) 반환! // 설명을 적을거에요 -> 풀차징은 직선 고정임을 + public float GetSpread(int lv) // 함수를 선언할거에요 -> 단계별 spread를 가져오는 GetSpread를 + { // 코드 블록을 시작할거에요 -> GetSpread 범위를 + if (lv == 3) return 0f; // 조건이 맞으면 반환할거에요 -> 3단계면 퍼짐 0(직선) + return (lv == 2) ? spreadLv2 : spreadLv1; // 값을 반환할거에요 -> 2단계면 spreadLv2, 아니면 spreadLv1 + } // 코드 블록을 끝낼거에요 -> GetSpread를 + +} // 코드 블록을 끝낼거에요 -> WeaponConfig를 \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/ChargeMonster.cs b/Assets/Scripts/Enemy/AI/ChargeMonster.cs index d4a32f69..03a484c5 100644 --- a/Assets/Scripts/Enemy/AI/ChargeMonster.cs +++ b/Assets/Scripts/Enemy/AI/ChargeMonster.cs @@ -1,211 +1,211 @@ -using UnityEngine; -using UnityEngine.AI; -using System.Collections; -using System.Collections.Generic; // ✅ 리스트 사용 필수 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 /// /// 돌진 공격 몬스터 /// -public class ChargeMonster : MonsterClass +public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ChargeMonster를 { - [Header("=== 돌진 공격 설정 ===")] - [SerializeField] private float chargeSpeed = 15f; - [SerializeField] private float chargeRange = 10f; - [SerializeField] private float chargeDuration = 2f; - [SerializeField] private float chargeDelay = 3f; - [SerializeField] private float prepareTime = 0.5f; + [Header("=== 돌진 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 돌진 공격 설정 === 을 + [SerializeField] private float chargeSpeed = 15f; // 변수를 선언할거에요 -> 돌진 속도(15.0)를 chargeSpeed에 + [SerializeField] private float chargeRange = 10f; // 변수를 선언할거에요 -> 돌진 시작 거리(10.0)를 chargeRange에 + [SerializeField] private float chargeDuration = 2f; // 변수를 선언할거에요 -> 돌진 지속 시간(2.0초)을 chargeDuration에 + [SerializeField] private float chargeDelay = 3f; // 변수를 선언할거에요 -> 돌진 쿨타임(3.0초)을 chargeDelay에 + [SerializeField] private float prepareTime = 0.5f; // 변수를 선언할거에요 -> 돌진 준비 시간(0.5초)을 prepareTime에 // 🎁 [추가] 드랍 아이템 리스트 - [Header("=== 드랍 아이템 설정 ===")] - [Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")] - [SerializeField] private List dropItemPrefabs; - [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] - [Range(0, 100)][SerializeField] private float dropChance = 30f; + [Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을 + [Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private List dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍 아이템 프리팹 목록을 dropItemPrefabs에 + [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에 - private float lastChargeTime; - private bool isCharging = false; - private bool isPreparing = false; - private Vector3 chargeDirection; + private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을 lastChargeTime에 + private bool isCharging = false; // 변수를 선언할거에요 -> 돌진 중 여부를 isCharging에 + private bool isPreparing = false; // 변수를 선언할거에요 -> 준비 중 여부를 isPreparing에 + private Vector3 chargeDirection; // 변수를 선언할거에요 -> 돌진 방향 벡터를 chargeDirection에 - [Header("공격 / 이동 애니메이션")] - [SerializeField] private string chargeAnimation = "Monster_Charge"; - [SerializeField] private string prepareAnimation = "Monster_ChargePrepare"; - [SerializeField] private string Monster_Walk = "Monster_Walk"; + [Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을 + [SerializeField] private string chargeAnimation = "Monster_Charge"; // 변수를 선언할거에요 -> 돌진 애니메이션 이름을 chargeAnimation에 + [SerializeField] private string prepareAnimation = "Monster_ChargePrepare"; // 변수를 선언할거에요 -> 준비 애니메이션 이름을 prepareAnimation에 + [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에 - [Header("AI 상세 설정")] - [SerializeField] private float patrolRadius = 5f; - [SerializeField] private float patrolInterval = 2f; + [Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을 + [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 + [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에 - private float nextPatrolTime; - private bool isPlayerInZone; - private Rigidbody _rigidbody; + private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 + private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에 + private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트를 담을 _rigidbody를 - protected override void Awake() + protected override void Awake() // 함수를 덮어씌워 실행할거에요 -> Awake 초기화 로직을 { - base.Awake(); - _rigidbody = GetComponent(); - if (_rigidbody == null) _rigidbody = gameObject.AddComponent(); - _rigidbody.isKinematic = true; + base.Awake(); // 부모의 함수를 실행할거에요 -> MonsterClass의 Awake를 + _rigidbody = GetComponent(); // 컴포넌트를 가져올거에요 -> Rigidbody를 + if (_rigidbody == null) _rigidbody = gameObject.AddComponent(); // 조건이 맞으면 실행할거에요 -> 없으면 새로 추가해서 _rigidbody에 + _rigidbody.isKinematic = true; // 설정을 바꿀거에요 -> 물리 연산을 꺼두기(Kinematic)로 } - protected override void Init() + protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기 설정을 Init에서 { - if (agent != null) agent.stoppingDistance = 1f; - if (animator != null) animator.applyRootMotion = false; + if (agent != null) agent.stoppingDistance = 1f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 1.0으로 + if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로 } - protected override void ExecuteAILogic() + protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { - if (isHit || isCharging || isPreparing) return; + if (isHit || isCharging || isPreparing) return; // 조건이 맞으면 중단할거에요 -> 피격, 돌진, 준비 중이라면 - float distance = Vector3.Distance(transform.position, playerTransform.position); + float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 - if (isPlayerInZone || distance <= chargeRange * 1.5f) + if (isPlayerInZone || distance <= chargeRange * 1.5f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면 { - HandleChargeCombat(distance); + HandleChargeCombat(distance); // 함수를 실행할거에요 -> 전투 로직인 HandleChargeCombat을 } - else + else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 { - Patrol(); + Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을 } - UpdateMovementAnimation(); + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateMovementAnimation을 } - void HandleChargeCombat(float distance) + void HandleChargeCombat(float distance) // 함수를 선언할거에요 -> 거리 기반 전투 판단 로직 HandleChargeCombat을 { - if (distance <= chargeRange && Time.time >= lastChargeTime + chargeDelay) + if (distance <= chargeRange && Time.time >= lastChargeTime + chargeDelay) // 조건이 맞으면 실행할거에요 -> 사거리 안이고 쿨타임이 지났다면 { - StartCoroutine(PrepareCharge()); + StartCoroutine(PrepareCharge()); // 코루틴을 실행할거에요 -> 돌진 준비인 PrepareCharge를 } - else if (!isCharging && !isPreparing) + else if (!isCharging && !isPreparing) // 조건이 맞으면 실행할거에요 -> 돌진이나 준비 중이 아니라면 (추격) { - if (agent.isOnNavMesh) + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { - if (agent.isStopped) agent.isStopped = false; - agent.SetDestination(playerTransform.position); + if (agent.isStopped) agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고 + agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 } } } - IEnumerator PrepareCharge() + IEnumerator PrepareCharge() // 코루틴 함수를 선언할거에요 -> 돌진 준비 과정인 PrepareCharge를 { - isPreparing = true; - if (agent.isOnNavMesh) + isPreparing = true; // 상태를 바꿀거에요 -> 준비 중 상태를 참(true)으로 + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { - agent.isStopped = true; - agent.ResetPath(); - agent.velocity = Vector3.zero; + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 + agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 초기화하라고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } - chargeDirection = (playerTransform.position - transform.position).normalized; - chargeDirection.y = 0; - if (chargeDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(chargeDirection); + chargeDirection = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을 + chargeDirection.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게 y를 0으로 + if (chargeDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(chargeDirection); // 회전시킬거에요 -> 돌진 방향을 바라보게 - if (!string.IsNullOrEmpty(prepareAnimation)) animator.Play(prepareAnimation, 0, 0f); + if (!string.IsNullOrEmpty(prepareAnimation)) animator.Play(prepareAnimation, 0, 0f); // 재생할거에요 -> 준비 애니메이션을 - Debug.Log("[ChargeMonster] 돌진 준비..."); - yield return new WaitForSeconds(prepareTime); + Debug.Log("[ChargeMonster] 돌진 준비..."); // 로그를 출력할거에요 -> 준비 메시지를 + yield return new WaitForSeconds(prepareTime); // 기다릴거에요 -> 준비 시간만큼 - StartCoroutine(Charge()); + StartCoroutine(Charge()); // 코루틴을 실행할거에요 -> 실제 돌진인 Charge를 } - IEnumerator Charge() + IEnumerator Charge() // 코루틴 함수를 선언할거에요 -> 실제 돌진 동작인 Charge를 { - isPreparing = false; - isCharging = true; - lastChargeTime = Time.time; + isPreparing = false; // 상태를 바꿀거에요 -> 준비 상태를 거짓(false)으로 + isCharging = true; // 상태를 바꿀거에요 -> 돌진 중 상태를 참(true)으로 + lastChargeTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 돌진 시간으로 - if (agent != null) agent.enabled = false; - if (_rigidbody != null) _rigidbody.isKinematic = false; + if (agent != null) agent.enabled = false; // 기능을 끌거에요 -> 길찾기 에이전트를 (물리 이동을 위해) + if (_rigidbody != null) _rigidbody.isKinematic = false; // 기능을 켤거에요 -> 물리 연산을 (충돌 감지를 위해) - animator.Play(chargeAnimation, 0, 0f); - Debug.Log("[ChargeMonster] 돌진 시작!"); + animator.Play(chargeAnimation, 0, 0f); // 재생할거에요 -> 돌진 애니메이션을 + Debug.Log("[ChargeMonster] 돌진 시작!"); // 로그를 출력할거에요 -> 돌진 시작 메시지를 - float chargeStartTime = Time.time; + float chargeStartTime = Time.time; // 값을 저장할거에요 -> 돌진 시작 시간을 - while (Time.time < chargeStartTime + chargeDuration) + while (Time.time < chargeStartTime + chargeDuration) // 반복할거에요 -> 지속 시간이 끝날 때까지 { - if (_rigidbody != null) - _rigidbody.velocity = chargeDirection * chargeSpeed; - else - transform.position += chargeDirection * chargeSpeed * Time.deltaTime; - yield return null; + if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면 + _rigidbody.velocity = chargeDirection * chargeSpeed; // 값을 넣을거에요 -> 속도를 돌진 방향과 속도로 + else // 조건이 틀리면 실행할거에요 -> 리지드바디가 없다면 (비상용) + transform.position += chargeDirection * chargeSpeed * Time.deltaTime; // 이동시킬거에요 -> 위치를 직접 수정해서 + yield return null; // 대기할거에요 -> 다음 프레임까지 } - if (_rigidbody != null) + if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면 { - _rigidbody.velocity = Vector3.zero; - _rigidbody.isKinematic = true; + _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 + _rigidbody.isKinematic = true; // 기능을 끌거에요 -> 물리 연산을 (다시 NavMesh 이동을 위해) } - if (agent != null) + if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 { - agent.enabled = true; - agent.ResetPath(); - agent.velocity = Vector3.zero; + agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를 + agent.ResetPath(); // 초기화할거에요 -> 경로를 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 내부 속도를 0으로 } - isCharging = false; - Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작"); + isCharging = false; // 상태를 바꿀거에요 -> 돌진 중 상태를 거짓(false)으로 + Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작"); // 로그를 출력할거에요 -> 종료 메시지를 } - void UpdateMovementAnimation() + void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을 { - if (isCharging || isPreparing || isHit || isResting) return; - if (agent.enabled && agent.velocity.magnitude > 0.1f) - animator.Play(Monster_Walk); - else - animator.Play(Monster_Idle); + if (isCharging || isPreparing || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 + if (agent.enabled && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면 + animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을 + else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면 + animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을 } - void Patrol() + void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을 { - if (Time.time < nextPatrolTime) return; - Vector3 randomPoint = transform.position + new Vector3( + if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면 + Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을 Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius) ); - if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) - if (agent.isOnNavMesh) agent.SetDestination(hit.position); + if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 NavMesh 위치라면 + if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그곳으로 이동하라고 - nextPatrolTime = Time.time + patrolInterval; + nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을 } - private void OnCollisionEnter(Collision collision) + private void OnCollisionEnter(Collision collision) // 함수를 실행할거에요 -> 물리 충돌이 발생했을 때 OnCollisionEnter를 { - if (!isCharging) return; - if (collision.gameObject.CompareTag("Player")) + if (!isCharging) return; // 조건이 맞으면 중단할거에요 -> 돌진 중이 아니라면 + if (collision.gameObject.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌한 대상이 플레이어라면 { - if (collision.gameObject.TryGetComponent(out var playerHealth)) + if (collision.gameObject.TryGetComponent(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를 { - if (!playerHealth.isInvincible) - playerHealth.TakeDamage(attackDamage); + if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면 + playerHealth.TakeDamage(attackDamage); // 함수를 실행할거에요 -> 데미지를 입히는 TakeDamage를 } } } // ☠️ [추가] 죽을 때 아이템 드랍 - protected override void OnDie() + protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 아이템 드랍 로직인 OnDie를 { - if (_rigidbody != null) _rigidbody.velocity = Vector3.zero; + if (_rigidbody != null) _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 죽을 때 미끄러지지 않게 속도를 0으로 - if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) + if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 아이템 리스트가 있다면 { - float randomValue = Random.Range(0f, 100f); - if (randomValue <= dropChance) + float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0~100 사이 랜덤값을 + if (randomValue <= dropChance) // 조건이 맞으면 실행할거에요 -> 확률에 당첨되었다면 { - int randomIndex = Random.Range(0, dropItemPrefabs.Count); - if (dropItemPrefabs[randomIndex] != null) + int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 랜덤 인덱스를 + if (dropItemPrefabs[randomIndex] != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면 { - Instantiate(dropItemPrefabs[randomIndex], transform.position + Vector3.up * 0.5f, Quaternion.identity); + Instantiate(dropItemPrefabs[randomIndex], transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에 } } } } - private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } - private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } + private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 감지 시 참(true)으로 + private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어가 나가면 거짓(false)으로 } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/ExplodeMonster.cs b/Assets/Scripts/Enemy/AI/ExplodeMonster.cs index d582d4b0..7a2bbdd2 100644 --- a/Assets/Scripts/Enemy/AI/ExplodeMonster.cs +++ b/Assets/Scripts/Enemy/AI/ExplodeMonster.cs @@ -1,211 +1,211 @@ -using UnityEngine; -using UnityEngine.AI; -using System.Collections; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 /// /// 자폭 몬스터 (Kamikaze) /// - 플레이어에게 전력 질주 /// - 범위 내 진입 시 제자리에서 카운트다운 후 폭발 /// -public class ExplodeMonster : MonsterClass +public class ExplodeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ExplodeMonster를 { - [Header("=== 자폭 설정 ===")] - [SerializeField] private float explodeRange = 4f; // 데미지 입히는 범위 (폭발 반경) - [SerializeField] private float triggerRange = 2.5f; // 폭발을 시작하는 거리 (이 안에 들어오면 카운트다운) - [SerializeField] private float fuseTime = 1.5f; // 지연 시간 (도망갈 기회 줌) - [SerializeField] private float explosionDamage = 50f; // 폭발 데미지 + [Header("=== 자폭 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 자폭 설정 === 을 + [SerializeField] private float explodeRange = 4f; // 변수를 선언할거에요 -> 폭발 데미지 반경(4.0)을 explodeRange에 + [SerializeField] private float triggerRange = 2.5f; // 변수를 선언할거에요 -> 자폭 시퀀스 시작 거리(2.5)를 triggerRange에 + [SerializeField] private float fuseTime = 1.5f; // 변수를 선언할거에요 -> 폭발 지연 시간(1.5초)을 fuseTime에 + [SerializeField] private float explosionDamage = 50f; // 변수를 선언할거에요 -> 폭발 데미지(50.0)를 explosionDamage에 - [Header("폭발 효과")] - [SerializeField] private GameObject explosionEffectPrefab; // 쾅! 이펙트 프리팹 - [SerializeField] private ParticleSystem fuseEffect; // 몸에서 불꽃 튀는 이펙트 (선택) - [SerializeField] private AudioClip fuseSound; // 치익~ 소리 - [SerializeField] private AudioClip explosionSound; // 쾅! 소리 + [Header("폭발 효과")] // 인스펙터 창에 제목을 표시할거에요 -> 폭발 효과 를 + [SerializeField] private GameObject explosionEffectPrefab; // 변수를 선언할거에요 -> 폭발 이펙트 프리팹을 explosionEffectPrefab에 + [SerializeField] private ParticleSystem fuseEffect; // 변수를 선언할거에요 -> 도화선(준비) 이펙트를 fuseEffect에 + [SerializeField] private AudioClip fuseSound; // 변수를 선언할거에요 -> 도화선 소리를 fuseSound에 + [SerializeField] private AudioClip explosionSound; // 변수를 선언할거에요 -> 폭발 소리를 explosionSound에 - [Header("애니메이션")] - [SerializeField] private string runAnimation = "Monster_Run"; // 달리기 - [SerializeField] private string fuseAnimation = "Monster_Fuse"; // 부들부들(폭발 준비) + [Header("애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 을 + [SerializeField] private string runAnimation = "Monster_Run"; // 변수를 선언할거에요 -> 달리기 애니메이션 이름을 runAnimation에 + [SerializeField] private string fuseAnimation = "Monster_Fuse"; // 변수를 선언할거에요 -> 자폭 준비 애니메이션 이름을 fuseAnimation에 - [Header("AI 설정")] - [SerializeField] private float chaseSpeed = 6f; // 이동 속도 (다른 애들보다 빨라야 무서움) - [SerializeField] private float patrolRadius = 5f; - [SerializeField] private float patrolInterval = 2f; + [Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을 + [SerializeField] private float chaseSpeed = 6f; // 변수를 선언할거에요 -> 추격 속도(6.0)를 chaseSpeed에 + [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 + [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에 - private bool isExploding = false; // 폭발 진행 중? - private bool hasExploded = false; // 이미 터졌나? - private float nextPatrolTime; - private bool isPlayerInZone; + private bool isExploding = false; // 변수를 초기화할거에요 -> 폭발 진행 중 여부를 거짓(false)으로 + private bool hasExploded = false; // 변수를 초기화할거에요 -> 이미 터졌는지 여부를 거짓(false)으로 + private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 + private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 초기화 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - protected override void Init() + protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을 { if (agent != null) { - agent.speed = chaseSpeed; // 속도 설정 - agent.stoppingDistance = 0.5f; // 바짝 붙음 + agent.speed = chaseSpeed; // 값을 설정할거에요 -> 이동 속도를 추격 속도로 + agent.stoppingDistance = 0.5f; // 값을 설정할거에요 -> 정지 거리를 아주 짧게(0.5) } - if (animator != null) animator.applyRootMotion = false; + if (animator != null) animator.applyRootMotion = false; // 설정을 바꿀거에요 -> 애니메이션 이동을 끄기로 - if (fuseEffect != null) fuseEffect.Stop(); + if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를 } - protected override void OnResetStats() + protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를 { - isExploding = false; - hasExploded = false; - if (fuseEffect != null) fuseEffect.Stop(); + isExploding = false; // 상태를 바꿀거에요 -> 폭발 진행 상태를 거짓(false)으로 + hasExploded = false; // 상태를 바꿀거에요 -> 폭발 완료 상태를 거짓(false)으로 + if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트를 끄기를 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // AI 로직 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - protected override void ExecuteAILogic() + protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { - if (isHit || isExploding || hasExploded) return; + if (isHit || isExploding || hasExploded) return; // 조건이 맞으면 중단할거에요 -> 피격, 자폭 중, 이미 폭발 상태라면 - float distance = Vector3.Distance(transform.position, playerTransform.position); + float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 // 플레이어가 감지 범위(15m) 안에 있거나 트리거에 닿았으면 추격 - if (isPlayerInZone || distance <= 15f) + if (isPlayerInZone || distance <= 15f) // 조건이 맞으면 실행할거에요 -> 추격 조건이 만족되면 { - ChasePlayer(distance); + ChasePlayer(distance); // 함수를 실행할거에요 -> 추격 및 자폭 처리를 하는 ChasePlayer를 } - else + else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 { - Patrol(); - UpdateMovementAnimation(); + Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을 + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션을 갱신하는 UpdateMovementAnimation을 } } - void ChasePlayer(float distance) + void ChasePlayer(float distance) // 함수를 선언할거에요 -> 거리별 추격 행동을 정하는 ChasePlayer를 { // 1. 폭발 시작 거리 안으로 들어왔다? -> 점화! - if (distance <= triggerRange) + if (distance <= triggerRange) // 조건이 맞으면 실행할거에요 -> 자폭 거리 이내라면 { - StartCoroutine(ExplodeRoutine()); - return; + StartCoroutine(ExplodeRoutine()); // 코루틴을 실행할거에요 -> 자폭 시퀀스인 ExplodeRoutine을 + return; // 중단할거에요 -> 더 이상 이동하지 않도록 } // 2. 아직 멀었다? -> 전력 질주 - if (agent.isOnNavMesh) + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { - agent.isStopped = false; - agent.SetDestination(playerTransform.position); + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고 + agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 // 달리기 애니메이션 - animator.Play(runAnimation); + animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을 } } - void UpdateMovementAnimation() + void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을 { - if (isExploding || isHit) return; + if (isExploding || isHit) return; // 조건이 맞으면 중단할거에요 -> 폭발 중이거나 맞고 있다면 - if (agent.velocity.magnitude > 0.1f) - animator.Play(runAnimation); - else - animator.Play(Monster_Idle); + if (agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면 + animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을 + else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면 + animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을 } - void Patrol() + void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을 { - if (Time.time < nextPatrolTime) return; + if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면 - Vector3 randomPoint = transform.position + new Vector3( + Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을 Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius) ); - if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) - if (agent.isOnNavMesh) agent.SetDestination(hit.position); + if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 + if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 이동하라고 - nextPatrolTime = Time.time + patrolInterval; + nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 💣 폭발 시퀀스 (점화 -> 대기 -> 쾅!) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - IEnumerator ExplodeRoutine() + IEnumerator ExplodeRoutine() // 코루틴 함수를 선언할거에요 -> 자폭 진행 과정인 ExplodeRoutine을 { - if (hasExploded) yield break; + if (hasExploded) yield break; // 조건이 맞으면 종료할거에요 -> 이미 터졌다면 - isExploding = true; - hasExploded = true; - isAttacking = true; // 부모 클래스 간섭 방지 + isExploding = true; // 상태를 바꿀거에요 -> 폭발 진행 상태를 참(true)으로 + hasExploded = true; // 상태를 바꿀거에요 -> 폭발 완료 상태를 참(true)으로 (중복 방지) + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 (부모 간섭 방지) // ⭐ [핵심] 급브레이크! (문워크 방지) - if (agent.isOnNavMesh) + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { - agent.isStopped = true; - agent.ResetPath(); // 목적지 삭제 - agent.velocity = Vector3.zero; // 속도 0 + agent.isStopped = true; // 명령을 내릴거에요 -> 멈추라고 + agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 지우라고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } - Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)"); + Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)"); // 로그를 출력할거에요 -> 자폭 예고 메시지를 // 1. 부들부들 떨기 (폭발 준비 모션) - if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation); + if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation); // 재생할거에요 -> 준비 애니메이션을 // 2. 치익~ 소리 & 이펙트 - if (fuseEffect != null) fuseEffect.Play(); - if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound); + if (fuseEffect != null) fuseEffect.Play(); // 실행할거에요 -> 도화선 이펙트를 + if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound); // 재생할거에요 -> 도화선 소리를 // 3. 도망갈 시간 주기 - yield return new WaitForSeconds(fuseTime); + yield return new WaitForSeconds(fuseTime); // 기다릴거에요 -> 폭발 지연 시간만큼 // 4. 쾅! - PerformExplosion(); + PerformExplosion(); // 함수를 실행할거에요 -> 실제 폭발 처리를 } - void PerformExplosion() + void PerformExplosion() // 함수를 선언할거에요 -> 폭발 데미지와 이펙트를 처리하는 PerformExplosion을 { - Debug.Log("💥💥💥 쾅!!!"); + Debug.Log("💥💥💥 쾅!!!"); // 로그를 출력할거에요 -> 폭발 메시지를 // 폭발 이펙트 생성 (Particle System) - if (explosionEffectPrefab != null) + if (explosionEffectPrefab != null) // 조건이 맞으면 실행할거에요 -> 폭발 프리팹이 있다면 { - Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); + Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 폭발 이펙트를 현재 위치에 } // 폭발음 - if (explosionSound != null) + if (explosionSound != null) // 조건이 맞으면 실행할거에요 -> 폭발음이 있다면 { - AudioSource.PlayClipAtPoint(explosionSound, transform.position, 1f); + AudioSource.PlayClipAtPoint(explosionSound, transform.position, 1f); // 재생할거에요 -> 폭발 소리를 해당 위치에서 } // 주변 데미지 처리 - DamageNearbyTargets(); + DamageNearbyTargets(); // 함수를 실행할거에요 -> 주변 대상에게 데미지를 주는 DamageNearbyTargets를 // 몬스터 사망 처리 (MonsterClass 기능 사용) - Die(); + Die(); // 함수를 실행할거에요 -> 몬스터를 죽게 만드는 Die를 } - void DamageNearbyTargets() + void DamageNearbyTargets() // 함수를 선언할거에요 -> 폭발 범위 내 데미지를 입히는 DamageNearbyTargets를 { // 폭발 범위(Sphere) 안에 있는 모든 물체 검사 - Collider[] hitColliders = Physics.OverlapSphere(transform.position, explodeRange); + Collider[] hitColliders = Physics.OverlapSphere(transform.position, explodeRange); // 배열에 담을거에요 -> 폭발 범위 내의 충돌체들을 - foreach (Collider hit in hitColliders) + foreach (Collider hit in hitColliders) // 반복할거에요 -> 감지된 모든 충돌체에 대해 { // 플레이어 데미지 - if (hit.CompareTag("Player")) + if (hit.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 대상이 플레이어라면 { - if (hit.TryGetComponent(out var playerHealth)) + if (hit.TryGetComponent(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를 { - if (!playerHealth.isInvincible) + if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면 { - playerHealth.TakeDamage(explosionDamage); - Debug.Log($"💥 플레이어 폭사! 데미지: {explosionDamage}"); + playerHealth.TakeDamage(explosionDamage); // 함수를 실행할거에요 -> 폭발 데미지를 입히는 TakeDamage를 + Debug.Log($"💥 플레이어 폭사! 데미지: {explosionDamage}"); // 로그를 출력할거에요 -> 피격 메시지를 } } } // (선택사항) 주변 몬스터도 팀킬? - else if (hit.CompareTag("Enemy") && hit.gameObject != gameObject) + else if (hit.CompareTag("Enemy") && hit.gameObject != gameObject) // 조건이 맞으면 실행할거에요 -> 대상이 다른 몬스터라면 { // 팀킬 로직이 필요하면 여기에 추가 } @@ -217,24 +217,24 @@ public class ExplodeMonster : MonsterClass // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 자폭병은 때려도 폭발 안 멈춤 (취향에 따라 수정 가능) - protected override void OnStartHit() { } + protected override void OnStartHit() { } // 함수를 비워둘거에요 -> 피격 시 아무 행동도 안 하게 (폭발 캔슬 방지) // 죽을 때 퓨즈 끄기 - protected override void OnDie() + protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 정리를 위해 { - if (fuseEffect != null && fuseEffect.isPlaying) fuseEffect.Stop(); + if (fuseEffect != null && fuseEffect.isPlaying) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를 } - private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } - private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } + private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 진입 시 감지 상태를 참으로 + private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어 이탈 시 감지 상태를 거짓으로 // 에디터에서 범위 보여주기 - private void OnDrawGizmosSelected() + private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 범위를 그리는 OnDrawGizmosSelected를 { - Gizmos.color = Color.red; - Gizmos.DrawWireSphere(transform.position, triggerRange); // 감지 범위 + Gizmos.color = Color.red; // 색상을 설정할거에요 -> 감지 범위 색을 빨간색으로 + Gizmos.DrawWireSphere(transform.position, triggerRange); // 그림을 그릴거에요 -> 자폭 감지 범위를 - Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); - Gizmos.DrawSphere(transform.position, explodeRange); // 폭발 데미지 범위 + Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); // 색상을 설정할거에요 -> 폭발 범위 색을 주황색 반투명으로 + Gizmos.DrawSphere(transform.position, explodeRange); // 그림을 그릴거에요 -> 폭발 데미지 범위를 } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/Monster Weapon.cs b/Assets/Scripts/Enemy/AI/Monster Weapon.cs index bb93aafe..511234dc 100644 --- a/Assets/Scripts/Enemy/AI/Monster Weapon.cs +++ b/Assets/Scripts/Enemy/AI/Monster Weapon.cs @@ -1,77 +1,80 @@ -using System.Collections.Generic; -using UnityEngine; +using System.Collections.Generic; // 제네릭 컬렉션을 쓰기 위해 불러올거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class MonsterWeapon : MonoBehaviour -{ - [Header("무기 설정")] - [Tooltip("이 무기 고유의 공격력 (예: 10)")] - [SerializeField] private float weaponBaseDamage = 10f; +public class MonsterWeapon : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterWeapon을 +{ // 코드 블록을 시작할거에요 -> MonsterWeapon 범위를 - private float _finalDamage; - [SerializeField] private BoxCollider _weaponCollider; + [Header("무기 설정")] // 인스펙터에 제목을 표시할거에요 -> 무기 설정을 + [Tooltip("이 무기 고유의 공격력 (예: 10)")] // 인스펙터에 툴팁을 달거에요 -> weaponBaseDamage 설명을 + [SerializeField] private float weaponBaseDamage = 10f; // 변수를 선언할거에요 -> 무기 기본 공격력을 10으로 - private void Awake() - { - _weaponCollider = GetComponent(); - _finalDamage = weaponBaseDamage; - //DisableHitBox(); - EnableHitBox(); - } + private float _finalDamage; // 변수를 선언할거에요 -> 최종 데미지를 저장할 _finalDamage를 + [SerializeField] private BoxCollider _weaponCollider; // 변수를 선언할거에요 -> 무기 콜라이더를 저장할 _weaponCollider를 - public void SetDamage(float monsterStrength) - { - _finalDamage = weaponBaseDamage + monsterStrength; - Debug.Log($"✅ [무기] 데미지 설정됨: 총 {_finalDamage}"); - } + private void Awake() // 함수를 선언할거에요 -> 시작 시 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + _weaponCollider = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 오브젝트의 BoxCollider를 찾아 _weaponCollider에 + _finalDamage = weaponBaseDamage; // 값을 넣을거에요 -> 최종 데미지를 기본 데미지로 초기화 + //DisableHitBox(); // 주석 처리할거에요 -> 시작 시 판정을 끄는 코드(현재는 안 씀) + EnableHitBox(); // 함수를 실행할거에요 -> 시작하자마자 판정을 켜기(현재 설정) + } // 코드 블록을 끝낼거에요 -> Awake를 - public void EnableHitBox() - { - Debug.Log("enabletest"); + public void SetDamage(float monsterStrength) // 함수를 선언할거에요 -> 몬스터 스탯을 받아 최종 데미지를 세팅하는 SetDamage를 + { // 코드 블록을 시작할거에요 -> SetDamage 범위를 + _finalDamage = weaponBaseDamage + monsterStrength; // 값을 계산할거에요 -> 기본 데미지 + 몬스터 힘으로 최종 데미지 설정 + Debug.Log($"✅ [무기] 데미지 설정됨: 총 {_finalDamage}"); // 로그를 찍을거에요 -> 최종 데미지가 얼마인지 + } // 코드 블록을 끝낼거에요 -> SetDamage를 - if (_weaponCollider != null) - { - Debug.Log("setcollider"); - _weaponCollider.enabled = true; - Debug.Log("🔴 [무기] 공격 판정 ON! (휘두르기 시작)"); // 로그 추가 - } - } + public void EnableHitBox() // 함수를 선언할거에요 -> 공격 판정을 켜는 EnableHitBox를 + { // 코드 블록을 시작할거에요 -> EnableHitBox 범위를 + Debug.Log("enabletest"); // 로그를 찍을거에요 -> 함수가 호출됐는지 확인용 - public void DisableHitBox() - { - if (_weaponCollider != null) - { - _weaponCollider.enabled = false; - Debug.Log("⚪ [무기] 공격 판정 OFF (휘두르기 끝)"); // 로그 추가 - } - } + if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 정상적으로 잡혔는지 + { // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리 + Debug.Log("setcollider"); // 로그를 찍을거에요 -> 콜라이더 enable 직전 확인용 + _weaponCollider.enabled = true; // 값을 바꿀거에요 -> 트리거 판정을 켜기 + Debug.Log("🔴 [무기] 공격 판정 ON! (휘두르기 시작)"); // 로그를 찍을거에요 -> 판정이 켜졌음을 + } // 코드 블록을 끝낼거에요 -> 콜라이더 처리 + } // 코드 블록을 끝낼거에요 -> EnableHitBox를 - // ⭐ 여기가 핵심! 닿는 모든 것을 기록함 - private void OnTriggerEnter(Collider other) - { - // 1. 무엇이든 닿으면 일단 로그를 찍음 (범인 색출) - Debug.Log($"💥 [충돌 감지] 무기가 닿은 것: {other.name} / 태그: {other.tag}"); + public void DisableHitBox() // 함수를 선언할거에요 -> 공격 판정을 끄는 DisableHitBox를 + { // 코드 블록을 시작할거에요 -> DisableHitBox 범위를 + if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 있는지 + { // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리 + _weaponCollider.enabled = false; // 값을 바꿀거에요 -> 트리거 판정을 끄기 + Debug.Log("⚪ [무기] 공격 판정 OFF (휘두르기 끝)"); // 로그를 찍을거에요 -> 판정이 꺼졌음을 + } // 코드 블록을 끝낼거에요 -> 콜라이더 처리 + } // 코드 블록을 끝낼거에요 -> DisableHitBox를 - if (other.CompareTag("Player")) - { - PlayerHealth playerHealth = other.GetComponent(); + // ⭐ 여기가 핵심! 닿는 모든 것을 기록함 // 설명을 적을거에요 -> 트리거 충돌 감지 핵심 구간임을 + private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 다른 콜라이더가 들어오면 호출되는 OnTriggerEnter를 + { // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를 + // 1. 무엇이든 닿으면 일단 로그를 찍음 (범인 색출) // 설명을 적을거에요 -> 뭐랑 부딪히는지 디버깅용 + Debug.Log($"💥 [충돌 감지] 무기가 닿은 것: {other.name} / 태그: {other.tag}"); // 로그를 찍을거에요 -> 충돌한 오브젝트 이름/태그를 - if (playerHealth != null) - { - if (!playerHealth.isInvincible) - { - playerHealth.TakeDamage(_finalDamage); - Debug.Log($"⚔️ [적중] 플레이어 체력 깎임! (-{_finalDamage})"); - } - else - { - Debug.Log("🛡️ [방어] 플레이어가 무적 상태입니다."); - } - DisableHitBox(); - } - else - { - Debug.LogError("⚠️ [오류] 플레이어 태그는 있는데 'PlayerHealth' 스크립트가 없습니다!"); - } - } - } -} \ No newline at end of file + if (other.CompareTag("Player")) // 조건을 검사할거에요 -> 닿은 대상이 Player 태그인지 + { // 코드 블록을 시작할거에요 -> 플레이어일 때 처리 + PlayerHealth playerHealth = other.GetComponent(); // 컴포넌트를 가져올거에요 -> 플레이어의 PlayerHealth를 찾아 playerHealth에 + + if (playerHealth != null) // 조건을 검사할거에요 -> PlayerHealth가 실제로 붙어있는지 + { // 코드 블록을 시작할거에요 -> PlayerHealth가 있을 때 처리 + if (!playerHealth.isInvincible) // 조건을 검사할거에요 -> 플레이어가 무적인지 아닌지 + { // 코드 블록을 시작할거에요 -> 무적이 아닐 때 처리 + playerHealth.TakeDamage(_finalDamage); // 함수를 실행할거에요 -> 플레이어에게 최종 데미지 적용 + Debug.Log($"⚔️ [적중] 플레이어 체력 깎임! (-{_finalDamage})"); // 로그를 찍을거에요 -> 데미지가 들어갔음을 + } // 코드 블록을 끝낼거에요 -> 무적 아닐 때 처리 + else // 조건이 아니면 실행할거에요 -> 무적이면 + { // 코드 블록을 시작할거에요 -> 무적일 때 처리 + Debug.Log("🛡️ [방어] 플레이어가 무적 상태입니다."); // 로그를 찍을거에요 -> 무적이라 데미지 안 들어감을 + } // 코드 블록을 끝낼거에요 -> 무적 처리 + + DisableHitBox(); // 함수를 실행할거에요 -> 한 번 맞추면 바로 판정 끄기(중복 타격 방지) + } // 코드 블록을 끝낼거에요 -> PlayerHealth 있을 때 처리 + else // 조건이 아니면 실행할거에요 -> PlayerHealth가 없으면 + { // 코드 블록을 시작할거에요 -> 오류 처리 + Debug.LogError("⚠️ [오류] 플레이어 태그는 있는데 'PlayerHealth' 스크립트가 없습니다!"); // 에러 로그를 찍을거에요 -> 컴포넌트 누락 경고 + } // 코드 블록을 끝낼거에요 -> 오류 처리 + } // 코드 블록을 끝낼거에요 -> 플레이어 처리 + } // 코드 블록을 끝낼거에요 -> OnTriggerEnter를 + +} // 코드 블록을 끝낼거에요 -> MonsterWeapon을 \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/MonsterClass.cs b/Assets/Scripts/Enemy/AI/MonsterClass.cs index 7ccc6fba..94b99598 100644 --- a/Assets/Scripts/Enemy/AI/MonsterClass.cs +++ b/Assets/Scripts/Enemy/AI/MonsterClass.cs @@ -1,155 +1,163 @@ -using UnityEngine; -using UnityEngine.AI; -using System.Collections; -using System; - - +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using System; // 기본 시스템 기능(Action 등)을 사용할거에요 -> System을 /// /// 몬스터 기본 클래스 (공통 기능만) /// - 공격 방식은 자식 클래스에서 구현 /// -public abstract class MonsterClass : MonoBehaviour, IDamageable +public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 MonsterClass를 { - [Header("--- 최적화 설정 ---")] - protected Renderer mobRenderer; - protected Transform playerTransform; - [SerializeField] protected float optimizationDistance = 40f; + [Header("--- 최적화 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최적화 설정 --- 을 + protected Renderer mobRenderer; // 변수를 선언할거에요 -> 몬스터의 렌더러(외형) 컴포넌트를 담을 mobRenderer를 + protected Transform playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 playerTransform을 + [SerializeField] protected float optimizationDistance = 40f; // 변수를 선언할거에요 -> 몬스터 최적화(AI 정지) 거리(40.0)를 optimizationDistance에 - [Header("몬스터 기본 스탯")] - [SerializeField] protected float maxHP = 100f; - [SerializeField] protected float attackDamage = 10f; - [SerializeField] protected int expReward = 10; - [SerializeField] protected float moveSpeed = 3.5f; + [Header("몬스터 기본 스탯")] // 인스펙터 창에 제목을 표시할거에요 -> 몬스터 기본 스탯 을 + [SerializeField] protected float maxHP = 100f; // 변수를 선언할거에요 -> 최대 체력(100.0)을 maxHP에 + [SerializeField] protected float attackDamage = 10f; // 변수를 선언할거에요 -> 공격력(10.0)을 attackDamage에 + [SerializeField] protected int expReward = 10; // 변수를 선언할거에요 -> 처치 시 경험치 보상(10)을 expReward에 + [SerializeField] protected float moveSpeed = 3.5f; // 변수를 선언할거에요 -> 이동 속도(3.5)를 moveSpeed에 - protected float currentHP; - public event Action OnHealthChanged; + protected float currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 currentHP를 + public event Action OnHealthChanged; // 이벤트를 선언할거에요 -> 체력이 변할 때 알릴 OnHealthChanged를 - [Header("전투 / 무기 (선택사항)")] + [Header("전투 / 무기 (선택사항)")] // 인스펙터 창에 제목을 표시할거에요 -> 전투 / 무기 (선택사항) 을 // ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨) - [SerializeField] protected MonsterWeapon myWeapon; + [SerializeField] protected MonsterWeapon myWeapon; // 변수를 선언할거에요 -> 몬스터가 장착한 무기 스크립트를 myWeapon에 - [Header("피격 / 사망 / 대기 애니메이션")] - [SerializeField] protected string Monster_Idle = "Monster_Idle"; - [SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; - [SerializeField] protected string Monster_Die = "Monster_Die"; + [Header("피격 / 사망 / 대기 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 피격 / 사망 / 대기 애니메이션 을 + [SerializeField] protected string Monster_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름("Monster_Idle")을 Monster_Idle에 + [SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; // 변수를 선언할거에요 -> 피격 애니메이션 이름("Monster_GetDamage")을 Monster_GetDamage에 + [SerializeField] protected string Monster_Die = "Monster_Die"; // 변수를 선언할거에요 -> 사망 애니메이션 이름("Monster_Die")을 Monster_Die에 - protected Animator animator; - protected NavMeshAgent agent; - protected AudioSource audioSource; - protected bool isHit, isDead, isAttacking; + protected Animator animator; // 변수를 선언할거에요 -> 애니메이션 제어 컴포넌트를 담을 animator를 + protected NavMeshAgent agent; // 변수를 선언할거에요 -> 길찾기 에이전트 컴포넌트를 담을 agent를 + protected AudioSource audioSource; // 변수를 선언할거에요 -> 소리 재생 컴포넌트를 담을 audioSource를 - public bool IsAggroed { get; protected set; } + // ⭐ [핵심] 자식 클래스에서 공유할 상태 변수들 (중복 선언 방지) + protected bool isHit; + protected bool isDead; + protected bool isAttacking; - [Header("AI 설정")] - [SerializeField] protected float attackRestDuration = 1.5f; - protected bool isResting; + public bool IsAggroed { get; protected set; } // 프로퍼티를 선언할거에요 -> 어그로(전투) 상태 여부를 외부에서 읽기만 가능하게 IsAggroed에 - public static System.Action OnMonsterKilled; + [Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을 + [SerializeField] protected float attackRestDuration = 1.5f; // 변수를 선언할거에요 -> 공격 후 대기 시간(1.5초)을 attackRestDuration에 + protected bool isResting; // 변수를 선언할거에요 -> 휴식 중인지 여부를 저장할 isResting을 - [Header("공통 사운드/이펙트")] - [SerializeField] protected AudioClip hitSound, deathSound; - [SerializeField] protected GameObject deathEffectPrefab; - [SerializeField] protected ParticleSystem hitEffect; - [SerializeField] protected Transform impactSpawnPoint; + public static System.Action OnMonsterKilled; // 정적 이벤트를 선언할거에요 -> 몬스터 처치 시 경험치를 전달할 OnMonsterKilled를 + + [Header("공통 사운드/이펙트")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 사운드/이펙트 를 + [SerializeField] protected AudioClip hitSound, deathSound; // 변수를 선언할거에요 -> 피격음과 사망음 오디오 클립을 + [SerializeField] protected GameObject deathEffectPrefab; // 변수를 선언할거에요 -> 사망 시 재생할 이펙트 프리팹을 + [SerializeField] protected ParticleSystem hitEffect; // 변수를 선언할거에요 -> 피격 시 재생할 파티클 시스템을 + [SerializeField] protected Transform impactSpawnPoint; // 변수를 선언할거에요 -> 이펙트가 생성될 위치를 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 생명주기 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - protected virtual void Awake() + protected virtual void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 호출되는 Awake를 { - mobRenderer = GetComponentInChildren(); - animator = GetComponent(); - agent = GetComponent(); - audioSource = GetComponent(); - if (agent != null) agent.speed = moveSpeed; + mobRenderer = GetComponentInChildren(); // 컴포넌트를 가져올거에요 -> 자식 오브젝트의 렌더러를 mobRenderer에 + animator = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 애니메이터를 animator에 + agent = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 길찾기 에이전트를 agent에 + audioSource = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 오디오 소스를 audioSource에 + if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 설정할거에요 -> 에이전트가 있다면 이동 속도를 moveSpeed로 } - protected virtual void OnEnable() + protected virtual void OnEnable() // 함수를 실행할거에요 -> 오브젝트가 활성화될 때 호출되는 OnEnable을 { - playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; - if (mobRenderer != null) mobRenderer.enabled = true; - Init(); - if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); + playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // 오브젝트를 찾을거에요 -> "Player" 태그를 가진 오브젝트의 위치를 playerTransform에 + if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 보이게 켜기(true)를 + Init(); // 함수를 실행할거에요 -> 자식 클래스에서 정의할 초기화 함수 Init을 + if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에 등록하기를 } - protected virtual void OnDisable() + protected virtual void OnDisable() // 함수를 실행할거에요 -> 오브젝트가 비활성화될 때 호출되는 OnDisable을 { - if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); + if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에서 제외하기를 } - public void ResetStats() + // ⭐ [수정] Update를 virtual로 선언하여 자식이 재정의 가능하게 함 + protected virtual void Update() { - isDead = false; - IsAggroed = false; - currentHP = maxHP; - OnHealthChanged?.Invoke(currentHP, maxHP); + OnManagedUpdate(); // 기본적으로 매니저 업데이트 로직을 수행 (일반 몬스터용) + } - Collider col = GetComponent(); - if (col != null) col.enabled = true; + public void ResetStats() // 함수를 선언할거에요 -> 몬스터 상태를 초기화하는 ResetStats를 + { + isDead = false; // 상태를 바꿀거에요 -> 사망 상태를 거짓(false)으로 + IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로 + currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로 + OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 변경되었음을 UI 등에 - if (agent != null) agent.speed = moveSpeed; // 스피드 초기화 + Collider col = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에 + if (col != null) col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 다시 켜기(true)를 - OnResetStats(); + if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 속도를 초기화하기를 + + OnResetStats(); // 함수를 실행할거에요 -> 자식 클래스의 추가 초기화 함수 OnResetStats를 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 추상 메서드 (자식이 반드시 구현) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - protected virtual void Init() { } - protected abstract void ExecuteAILogic(); - protected virtual void OnResetStats() { } + protected virtual void Init() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 초기화 함수 Init을 + protected abstract void ExecuteAILogic(); // 추상 함수를 선언할거에요 -> 자식에서 반드시 구현해야 할 AI 로직 ExecuteAILogic을 + protected virtual void OnResetStats() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 스탯 초기화 함수 OnResetStats를 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 공통 기능 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - public virtual void Reactivate() + public virtual void Reactivate() // 함수를 선언할거에요 -> 몬스터를 재활성화하는 Reactivate를 { - if (agent != null && agent.isOnNavMesh) + if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 있고 바닥에 있다면 { - agent.isStopped = false; - if (IsAggroed && playerTransform != null) + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고 + if (IsAggroed && playerTransform != null) // 조건이 맞으면 실행할거에요 -> 어그로가 끌렸고 플레이어가 있다면 { - agent.SetDestination(playerTransform.position); + agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 } } - if (mobRenderer != null) mobRenderer.enabled = true; + if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 다시 켜기(true)를 } - public void OnManagedUpdate() + public virtual void OnManagedUpdate() // 함수를 선언할거에요 -> 매니저가 호출해줄 업데이트 함수 OnManagedUpdate를 { - if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; + if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; // 조건이 맞으면 중단할거에요 -> 죽었거나, 플레이어가 없거나, 꺼져 있다면 - float distance = Vector3.Distance(transform.position, playerTransform.position); + float distance = Vector3.Distance(transform.position, playerTransform.position); // 값을 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에 - if (distance > optimizationDistance && !IsAggroed) + if (distance > optimizationDistance && !IsAggroed) // 조건이 맞으면 실행할거에요 -> 거리가 멀고 어그로 상태가 아니라면 { - StopMovement(); - if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; - return; + StopMovement(); // 함수를 실행할거에요 -> 움직임을 멈추는 StopMovement를 + if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; // 조건이 맞으면 실행할거에요 -> 렌더러가 켜져 있다면 끄기(false)를 (최적화) + return; // 중단할거에요 -> AI 로직을 실행하지 않도록 함수를 } - if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; - if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } - if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; + if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 꺼져 있다면 다시 켜기(true)를 + if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } // 조건이 맞으면 실행할거에요 -> 렌더러가 화면에 보이지 않는다면 멈추고 중단하기를 + if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 멈춰있다면 다시 움직이게(false) 하기를 - ExecuteAILogic(); + ExecuteAILogic(); // 함수를 실행할거에요 -> 실제 몬스터의 AI 로직을 } - protected void StopMovement() + protected void StopMovement() // 함수를 선언할거에요 -> 이동을 멈추는 StopMovement를 { - if (agent != null && agent.isOnNavMesh) + if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상 작동 중이라면 { - agent.isStopped = true; - agent.velocity = Vector3.zero; + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 0으로 } - if (animator != null) + if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 { - animator.SetFloat("Speed", 0f); - animator.Play(Monster_Idle); + animator.SetFloat("Speed", 0f); // 값을 전달할거에요 -> 속도 파라미터를 0으로 + animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션(Monster_Idle)을 } } @@ -157,210 +165,195 @@ public abstract class MonsterClass : MonoBehaviour, IDamageable // 피격 / 사망 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - public virtual void TakeDamage(float amount) + public virtual void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 줄 때 호출할 TakeDamage를 { - OnDamaged(amount); + OnDamaged(amount); // 함수를 실행할거에요 -> 실제 피격 처리를 담당하는 OnDamaged를 } - public virtual void OnDamaged(float damage) + public virtual void OnDamaged(float damage) // 함수를 선언할거에요 -> 데미지 계산 로직인 OnDamaged를 { - if (isDead) return; - IsAggroed = true; - currentHP -= damage; - OnHealthChanged?.Invoke(currentHP, maxHP); - if (currentHP <= 0) { Die(); return; } - if (!isHit) StartHit(); + if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면 + IsAggroed = true; // 상태를 바꿀거에요 -> 공격받았으니 어그로 상태를 참(true)으로 + currentHP -= damage; // 값을 뺄거에요 -> 체력에서 데미지(damage)만큼을 + OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 깎였음을 + if (currentHP <= 0) { Die(); return; } // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 함수(Die)를 + if (!isHit) StartHit(); // 조건이 맞으면 실행할거에요 -> 피격 상태가 아니라면 피격 연출(StartHit)을 } - protected virtual void StartHit() + protected virtual void StartHit() // 함수를 선언할거에요 -> 피격 연출을 시작하는 StartHit을 { - isHit = true; - isAttacking = false; - isResting = false; - StopAllCoroutines(); + isHit = true; // 상태를 바꿀거에요 -> 피격 중 상태를 참(true)으로 + isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로 + isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 + StopAllCoroutines(); // 중단할거에요 -> 실행 중인 모든 코루틴을 - OnStartHit(); + OnStartHit(); // 함수를 실행할거에요 -> 자식 클래스의 추가 피격 처리 OnStartHit을 - if (agent && agent.isOnNavMesh) + if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면 { - agent.isStopped = true; - agent.velocity = Vector3.zero; + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } - animator.Play(Monster_GetDamage, 0, 0f); - if (hitEffect) hitEffect.Play(); - if (hitSound) audioSource.PlayOneShot(hitSound); + animator.Play(Monster_GetDamage, 0, 0f); // 재생할거에요 -> 피격 애니메이션(Monster_GetDamage)을 + if (hitEffect) hitEffect.Play(); // 조건이 맞으면 실행할거에요 -> 피격 이펙트가 있다면 재생하기를 + if (hitSound) audioSource.PlayOneShot(hitSound); // 조건이 맞으면 실행할거에요 -> 피격 사운드가 있다면 재생하기를 } - protected virtual void OnStartHit() { } + protected virtual void OnStartHit() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 피격 시작 함수 OnStartHit을 - public virtual void OnHitEnd() + public virtual void OnHitEnd() // 함수를 선언할거에요 -> 피격 연출이 끝났을 때 호출할 OnHitEnd를 { - isHit = false; - if (agent && agent.isOnNavMesh) agent.isStopped = false; + isHit = false; // 상태를 바꿀거에요 -> 피격 중 상태를 거짓(false)으로 + if (agent && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 다시 움직이게(false) 하기를 } - protected virtual void Die() + protected virtual void Die() // 함수를 선언할거에요 -> 몬스터 사망을 처리하는 Die를 { - if (isDead) return; - isDead = true; - IsAggroed = false; + if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽은 상태라면 + isDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참(true)으로 + IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로 - OnDie(); + OnDie(); // 함수를 실행할거에요 -> 자식 클래스의 추가 사망 처리 OnDie를 - OnMonsterKilled?.Invoke(expReward); - Collider col = GetComponent(); - if (col != null) col.enabled = false; + OnMonsterKilled?.Invoke(expReward); // 이벤트를 알릴거에요 -> 몬스터 처치와 경험치 보상(expReward)을 + Collider col = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에 + if (col != null) col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 끄기(false)를 (시체 밟기 방지) - if (agent && agent.isOnNavMesh) + if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면 { - agent.isStopped = true; - agent.velocity = Vector3.zero; + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } - animator.Play(Monster_Die, 0, 0f); - if (deathSound) audioSource.PlayOneShot(deathSound); - Invoke("ReturnToPool", 1.5f); + animator.Play(Monster_Die, 0, 0f); // 재생할거에요 -> 사망 애니메이션(Monster_Die)을 + if (deathSound) audioSource.PlayOneShot(deathSound); // 조건이 맞으면 실행할거에요 -> 사망 사운드가 있다면 재생하기를 + Invoke("ReturnToPool", 1.5f); // 예약을 걸거에요 -> 1.5초 뒤에 풀로 반환하는 ReturnToPool 함수를 } - protected virtual void OnDie() { } + protected virtual void OnDie() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 사망 처리 함수 OnDie를 - public bool IsDead => isDead; - protected void ReturnToPool() => gameObject.SetActive(false); + public bool IsDead => isDead; // 프로퍼티를 선언할거에요 -> 외부에서 사망 여부를 확인할 수 있는 IsDead를 + protected void ReturnToPool() => gameObject.SetActive(false); // 함수를 선언할거에요 -> 오브젝트를 비활성화해서 풀로 돌려보내는 ReturnToPool을 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ⭐ 공격 이벤트 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - public virtual void OnAttackStart() + // ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언 + public virtual void OnAttackStart() // 함수를 선언할거에요 -> 공격 시작 시 호출되는 OnAttackStart를 { - isAttacking = true; - isResting = false; + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 + isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 - if (myWeapon != null) + if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면 { - myWeapon.EnableHitBox(); + myWeapon.EnableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 켜는 EnableHitBox를 } } - public virtual void OnAttackEnd() + // ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언 + public virtual void OnAttackEnd() // 함수를 선언할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를 { - if (myWeapon != null) + if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면 { - myWeapon.DisableHitBox(); + myWeapon.DisableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 끄는 DisableHitBox를 } - isAttacking = false; - if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); + isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로 + if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴(RestAfterAttack)을 } - protected virtual IEnumerator RestAfterAttack() + protected virtual IEnumerator RestAfterAttack() // 코루틴 함수를 선언할거에요 -> 공격 후 잠시 쉬는 RestAfterAttack을 { - isResting = true; - yield return new WaitForSeconds(attackRestDuration); - isResting = false; + isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로 + yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간(attackRestDuration)만큼 + isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // ⭐ 상태 이상 시스템 (추가된 부분) + // ⭐ 상태 이상 시스템 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - /// - /// 상태이상 적용 (PlayerArrow에서 호출) - /// - public void ApplyStatusEffect(StatusEffectType type, float damage, float duration) + public void ApplyStatusEffect(StatusEffectType type, float damage, float duration) // 함수를 선언할거에요 -> 상태이상을 적용하는 ApplyStatusEffect를 { - if (isDead) return; + if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면 - switch (type) + switch (type) // 분기할거에요 -> 상태이상 종류(type)에 따라 { - case StatusEffectType.Burn: - StartCoroutine(BurnCoroutine(damage, duration)); + case StatusEffectType.Burn: // 조건이 맞으면 실행할거에요 -> 화상(Burn)이라면 + StartCoroutine(BurnCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 화상 효과를 주는 BurnCoroutine을 break; - case StatusEffectType.Slow: - StartCoroutine(SlowCoroutine(damage, duration)); + case StatusEffectType.Slow: // 조건이 맞으면 실행할거에요 -> 슬로우(Slow)라면 + StartCoroutine(SlowCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 느리게 만드는 SlowCoroutine을 break; - case StatusEffectType.Poison: - StartCoroutine(PoisonCoroutine(damage, duration)); + case StatusEffectType.Poison: // 조건이 맞으면 실행할거에요 -> 독(Poison)이라면 + StartCoroutine(PoisonCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 독 효과를 주는 PoisonCoroutine을 break; - case StatusEffectType.Shock: - TakeDamage(damage); // 충격 데미지 즉시 적용 - StartCoroutine(StunCoroutine(0.5f)); // 0.5초 스턴 + case StatusEffectType.Shock: // 조건이 맞으면 실행할거에요 -> 충격(Shock)이라면 + TakeDamage(damage); // 함수를 실행할거에요 -> 충격 데미지를 즉시 적용하기를 + StartCoroutine(StunCoroutine(0.5f)); // 코루틴을 실행할거에요 -> 0.5초간 기절시키는 StunCoroutine을 break; } } - /// - /// 화상: 일정 시간 동안 0.5초마다 틱 데미지 - /// - private IEnumerator BurnCoroutine(float tickDamage, float duration) + private IEnumerator BurnCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 화상 효과를 처리할 BurnCoroutine을 { - float elapsed = 0f; - float tickInterval = 0.5f; - while (elapsed < duration) + float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로 + float tickInterval = 0.5f; // 변수를 초기화할거에요 -> 데미지 간격을 0.5초로 + while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안 { - TakeDamage(tickDamage * tickInterval); - yield return new WaitForSeconds(tickInterval); - elapsed += tickInterval; + TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를 + yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 0.5초의 시간을 + elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 0.5초를 } } - /// - /// 슬로우: 이동속도를 일시적으로 감소 - /// - private IEnumerator SlowCoroutine(float amount, float duration) + private IEnumerator SlowCoroutine(float amount, float duration) // 코루틴 함수를 선언할거에요 -> 슬로우 효과를 처리할 SlowCoroutine을 { - // NavMeshAgent가 있는 경우 속도 조절 - if (agent != null) + if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 { - float originalSpeed = agent.speed; - agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); - yield return new WaitForSeconds(duration); - agent.speed = originalSpeed; // 원래 속도로 복구 + float originalSpeed = agent.speed; // 값을 저장할거에요 -> 원래 속도를 originalSpeed에 + agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); // 값을 변경할거에요 -> 현재 속도를 비율(amount)만큼 줄여서 + yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼 + agent.speed = originalSpeed; // 값을 복구할거에요 -> 속도를 원래대로 } - else + else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면 { - // NavMeshAgent가 없으면 그냥 대기 - yield return new WaitForSeconds(duration); + yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를 } } - /// - /// 독: 지속 데미지 (1초마다) - /// - private IEnumerator PoisonCoroutine(float tickDamage, float duration) + private IEnumerator PoisonCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 독 효과를 처리할 PoisonCoroutine을 { - float elapsed = 0f; - float tickInterval = 1f; - while (elapsed < duration) + float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로 + float tickInterval = 1f; // 변수를 초기화할거에요 -> 데미지 간격을 1초로 + while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안 { - TakeDamage(tickDamage * tickInterval); - yield return new WaitForSeconds(tickInterval); - elapsed += tickInterval; + TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를 + yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 1초의 시간을 + elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 1초를 } } - /// - /// 스턴: 짧은 시간 동안 행동 정지 - /// - private IEnumerator StunCoroutine(float duration) + private IEnumerator StunCoroutine(float duration) // 코루틴 함수를 선언할거에요 -> 기절 효과를 처리할 StunCoroutine을 { - if (agent != null) + if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 { - agent.isStopped = true; - if (animator != null) animator.speed = 0; // 애니메이션도 멈춤 (선택사항) + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고 + if (animator != null) animator.speed = 0; // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 재생 속도를 0으로 (얼음 땡) - yield return new WaitForSeconds(duration); + yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼 - if (!isDead) + if (!isDead) // 조건이 맞으면 실행할거에요 -> 아직 살아있다면 { - agent.isStopped = false; - if (animator != null) animator.speed = 1; + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고 + if (animator != null) animator.speed = 1; // 조건이 맞으면 실행할거에요 -> 애니메이션 속도를 정상(1)으로 } } - else + else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면 { - yield return new WaitForSeconds(duration); + yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를 } } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/NormalMonster.cs b/Assets/Scripts/Enemy/AI/NormalMonster.cs index 78af6565..1ff83c12 100644 --- a/Assets/Scripts/Enemy/AI/NormalMonster.cs +++ b/Assets/Scripts/Enemy/AI/NormalMonster.cs @@ -1,185 +1,182 @@ -using UnityEngine; -using UnityEngine.AI; -using System.Collections; -using System.Collections.Generic; // ✅ 리스트 사용을 위해 필수 추가 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 -public class MeleeMonster : MonsterClass +public class MeleeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 MeleeMonster를 { - [Header("=== 근접 공격 설정 ===")] - //[SerializeField] private MonsterWeapon myWeapon; // ✅ 주석 해제됨 (이제 무기 연결 가능!) - [SerializeField] private float attackRange = 2f; - [SerializeField] private float attackDelay = 1.5f; + [Header("=== 근접 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 근접 공격 설정 === 을 + [SerializeField] private float attackRange = 2f; // 변수를 선언할거에요 -> 공격 사거리(2.0)를 attackRange에 + [SerializeField] private float attackDelay = 1.5f; // 변수를 선언할거에요 -> 공격 딜레이(1.5초)를 attackDelay에 - // 🎁 [수정] 단일 아이템 -> 리스트로 변경 - [Header("=== 드랍 아이템 설정 ===")] - [Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")] - [SerializeField] private List dropItemPrefabs; + [Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을 + [Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private List dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍할 아이템 프리팹 목록을 dropItemPrefabs에 - [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] - [Range(0, 100)][SerializeField] private float dropChance = 30f; // 기본 30% + [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에 - private float lastAttackTime; + private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에 - [Header("공격 / 이동 애니메이션")] - [SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; - [SerializeField] private string Monster_Walk = "Monster_Walk"; + [Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을 + [SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; // 배열을 선언할거에요 -> 공격 애니메이션 이름들을 attackAnimations에 + [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에 - [Header("AI 상세 설정")] - [SerializeField] private float stopBuffer = 0.3f; - [SerializeField] private float patrolRadius = 5f; - [SerializeField] private float patrolInterval = 2f; + [Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을 + [SerializeField] private float stopBuffer = 0.3f; // 변수를 선언할거에요 -> 정지 여유 거리(0.3)를 stopBuffer에 + [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 + [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 대기 시간(2.0)을 patrolInterval에 - private float nextPatrolTime; - private float repathInterval = 0.3f; - private float nextRepathTime; - private int attackIndex; - private bool isPlayerInZone; + private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 + private float repathInterval = 0.3f; // 변수를 선언할거에요 -> 경로 갱신 주기(0.3초)를 repathInterval에 + private float nextRepathTime; // 변수를 선언할거에요 -> 다음 경로 갱신 시간을 nextRepathTime에 + private int attackIndex; // 변수를 선언할거에요 -> 현재 공격 애니메이션 인덱스를 attackIndex에 + private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어가 감지 구역에 있는지 여부를 isPlayerInZone에 - protected override void Init() + protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을 { - if (agent != null) agent.stoppingDistance = attackRange - 0.4f; - if (animator != null) animator.applyRootMotion = false; + if (agent != null) agent.stoppingDistance = attackRange - 0.4f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 사거리보다 약간 짧게 + if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동(Root Motion)을 끄기로 } - protected override void OnResetStats() + protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를 { - if (myWeapon != null) + if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면 { - myWeapon.SetDamage(attackDamage); + myWeapon.SetDamage(attackDamage); // 함수를 실행할거에요 -> 무기 공격력을 몬스터 공격력으로 설정을 } } - protected override void ExecuteAILogic() + protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { - if (isHit || isAttacking || isResting) return; + if (isHit || isAttacking || isResting) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식 중이라면 - float distance = Vector3.Distance(transform.position, playerTransform.position); + float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에 - if (isPlayerInZone || distance <= attackRange * 2f) - HandlePlayerTarget(); - else - Patrol(); + if (isPlayerInZone || distance <= attackRange * 2f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면 + HandlePlayerTarget(); // 함수를 실행할거에요 -> 추격 및 공격 처리를 하는 HandlePlayerTarget을 + else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 + Patrol(); // 함수를 실행할거에요 -> 주변을 배회하는 Patrol을 - UpdateMovementAnimation(); + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 상태를 갱신하는 UpdateMovementAnimation을 } - void HandlePlayerTarget() + void HandlePlayerTarget() // 함수를 선언할거에요 -> 타겟 처리 로직인 HandlePlayerTarget을 { - float distance = Vector3.Distance(transform.position, playerTransform.position); + float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 - if (distance <= attackRange - stopBuffer) - TryAttack(); - else if (Time.time >= nextRepathTime) + if (distance <= attackRange - stopBuffer) // 조건이 맞으면 실행할거에요 -> 사거리 안쪽으로 충분히 들어왔다면 + TryAttack(); // 함수를 실행할거에요 -> 공격을 시도하는 TryAttack을 + else if (Time.time >= nextRepathTime) // 조건이 맞으면 실행할거에요 -> 경로 갱신 시간이 되었다면 { - if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position); - nextRepathTime = Time.time + repathInterval; + if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 + nextRepathTime = Time.time + repathInterval; // 값을 갱신할거에요 -> 다음 경로 갱신 시간을 } } - void TryAttack() + void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을 { - if (Time.time < lastAttackTime + attackDelay) return; - lastAttackTime = Time.time; + if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 아직 쿨타임이 안 지났다면 + lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로 - string attackName = attackAnimations[attackIndex]; - attackIndex = (attackIndex + 1) % attackAnimations.Length; + string attackName = attackAnimations[attackIndex]; // 값을 가져올거에요 -> 현재 인덱스의 공격 애니메이션 이름을 + attackIndex = (attackIndex + 1) % attackAnimations.Length; // 값을 갱신할거에요 -> 다음 공격 인덱스로 (순환) - isAttacking = true; + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 - if (agent.isOnNavMesh) + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 바닥에 있다면 { - agent.isStopped = true; - agent.velocity = Vector3.zero; + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } - animator.Play(attackName, 0, 0f); + animator.Play(attackName, 0, 0f); // 재생할거에요 -> 공격 애니메이션을 처음부터 } - void UpdateMovementAnimation() + void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션을 제어하는 UpdateMovementAnimation을 { - if (isAttacking || isHit || isResting) return; + if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 - if (agent.velocity.magnitude < 0.1f) - animator.Play(Monster_Idle); - else - animator.Play(Monster_Walk); + if (agent.velocity.magnitude < 0.1f) // 조건이 맞으면 실행할거에요 -> 거의 멈춰 있다면 + animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을 + else // 조건이 틀리면 실행할거에요 -> 움직이고 있다면 + animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을 } - void Patrol() + void Patrol() // 함수를 선언할거에요 -> 순찰 로직인 Patrol을 { - if (Time.time < nextPatrolTime) return; + if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 아직 대기 시간이라면 - Vector3 randomPoint = transform.position + new Vector3( + Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 현재 위치 주변의 랜덤한 지점을 Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius) ); - if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) - if (agent.isOnNavMesh) agent.SetDestination(hit.position); + if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 랜덤 지점이 NavMesh 위라면 + if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그 지점으로 이동하라고 - nextPatrolTime = Time.time + patrolInterval; + nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을 } - public override void OnAttackStart() + public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 시 호출되는 OnAttackStart를 { - isAttacking = true; - isResting = false; - if (myWeapon != null) myWeapon.EnableHitBox(); + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 + isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 + if (myWeapon != null) myWeapon.EnableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 켜기를 } - public override void OnAttackEnd() + public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를 { - if (myWeapon != null) myWeapon.DisableHitBox(); - isAttacking = false; - if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); + if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 끄기를 + isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로 + if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴을 } - protected override IEnumerator RestAfterAttack() + protected override IEnumerator RestAfterAttack() // 코루틴 함수를 덮어씌워 실행할거에요 -> 공격 후 휴식하는 RestAfterAttack을 { - isResting = true; - yield return new WaitForSeconds(attackRestDuration); - isResting = false; + isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로 + yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간만큼 + isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 } - protected override void OnStartHit() + protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 피격 시작 시 호출되는 OnStartHit을 { - if (myWeapon != null) myWeapon.DisableHitBox(); + if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 피격 당했으니 공격 판정을 끄기를 } // 🎲 [핵심 수정] 리스트에서 랜덤 뽑기 + 확률 체크 - protected override void OnDie() + protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 호출되는 OnDie를 { - if (myWeapon != null) myWeapon.DisableHitBox(); + if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 공격 판정을 끄기를 // 1. 리스트에 아이템이 하나라도 있는지 확인 - if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) + if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 드랍 테이블이 비어있지 않다면 { // 2. 확률 체크 (0 ~ 100) - float randomValue = Random.Range(0f, 100f); + float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0부터 100 사이의 랜덤값을 - if (randomValue <= dropChance) // 당첨! + if (randomValue <= dropChance) // 당첨! // 조건이 맞으면 실행할거에요 -> 랜덤값이 드랍 확률 이하라면 { // 3. 리스트에서 랜덤하게 하나 뽑기 (0번 ~ 마지막 번호 중 하나) - int randomIndex = Random.Range(0, dropItemPrefabs.Count); - GameObject selectedItem = dropItemPrefabs[randomIndex]; + int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 아이템 리스트 인덱스를 랜덤으로 + GameObject selectedItem = dropItemPrefabs[randomIndex]; // 오브젝트를 가져올거에요 -> 선택된 아이템 프리팹을 - if (selectedItem != null) + if (selectedItem != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면 { - Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity); - // Debug.Log($"🎉 아이템 드랍! ({selectedItem.name})"); + Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에 } } } } - private void OnTriggerEnter(Collider other) + private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거에 들어왔을 때 OnTriggerEnter를 { - if (other.CompareTag("Player")) isPlayerInZone = true; + if (other.CompareTag("Player")) isPlayerInZone = true; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 참(true)으로 } - private void OnTriggerExit(Collider other) + private void OnTriggerExit(Collider other) // 함수를 실행할거에요 -> 트리거에서 나갔을 때 OnTriggerExit을 { - if (other.CompareTag("Player")) isPlayerInZone = false; + if (other.CompareTag("Player")) isPlayerInZone = false; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 거짓(false)으로 } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/Range Monster.cs b/Assets/Scripts/Enemy/AI/Range Monster.cs index 712e30bd..66a3d0d3 100644 --- a/Assets/Scripts/Enemy/AI/Range Monster.cs +++ b/Assets/Scripts/Enemy/AI/Range Monster.cs @@ -1,213 +1,208 @@ -using UnityEngine; -using UnityEngine.AI; -using System.Collections; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 /// /// 통합 원거리 몬스터 (키 작은 플레이어 머리 조준 기능 추가) /// 파일 이름을 반드시 [ UniversalRangedMonster.cs ] 로 맞춰주세요! /// -public class UniversalRangedMonster : MonsterClass +public class UniversalRangedMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 UniversalRangedMonster를 { - public enum AttackStyle { Straight, Lob } + public enum AttackStyle { Straight, Lob } // 열거형을 정의할거에요 -> 공격 방식(직선, 곡사)을 정의하는 AttackStyle을 - [Header("=== 🏹 공격 스타일 선택 ===")] - [SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; + [Header("=== 🏹 공격 스타일 선택 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 🏹 공격 스타일 선택 === 을 + [SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; // 변수를 선언할거에요 -> 공격 스타일(기본 직선)을 attackStyle에 - [Header("공통 설정")] - [SerializeField] private GameObject projectilePrefab; - [SerializeField] private Transform firePoint; - [SerializeField] private float attackRange = 10f; - [SerializeField] private float attackDelay = 2f; - [SerializeField] private float detectRange = 15f; + [Header("공통 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 설정 을 + [SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 발사체 프리팹을 projectilePrefab에 + [SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 발사 위치를 firePoint에 + [SerializeField] private float attackRange = 10f; // 변수를 선언할거에요 -> 공격 사거리(10.0)를 attackRange에 + [SerializeField] private float attackDelay = 2f; // 변수를 선언할거에요 -> 공격 딜레이(2.0초)를 attackDelay에 + [SerializeField] private float detectRange = 15f; // 변수를 선언할거에요 -> 인식 거리(15.0)를 detectRange에 - [Header("🔹 직선 발사 설정 (활/총)")] - [SerializeField] private float projectileSpeed = 20f; - [SerializeField] private float minDistance = 5f; + [Header("🔹 직선 발사 설정 (활/총)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔹 직선 발사 설정 (활/총) 을 + [SerializeField] private float projectileSpeed = 20f; // 변수를 선언할거에요 -> 투사체 속도(20.0)를 projectileSpeed에 + [SerializeField] private float minDistance = 5f; // 변수를 선언할거에요 -> 최소 거리(도망가는 거리)를 minDistance에 - [Header("🔸 곡사 투척 설정 (돌/폭탄)")] - [Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")] - [SerializeField] private bool usePreciseLob = true; + [Header("🔸 곡사 투척 설정 (돌/폭탄)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔸 곡사 투척 설정 (돌/폭탄) 을 + [Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private bool usePreciseLob = true; // 변수를 선언할거에요 -> 정밀 곡사 사용 여부를 usePreciseLob에 - [Tooltip("던지는 각도 (45도가 최대 사거리)")] - [Range(15f, 75f)][SerializeField] private float launchAngle = 45f; + [Tooltip("던지는 각도 (45도가 최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(15f, 75f)][SerializeField] private float launchAngle = 45f; // 변수를 선언할거에요 -> 발사 각도(45도)를 launchAngle에 // ⭐ [추가됨] 조준 높이 보정 (키 작은 플레이어 해결용) - [Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")] - [SerializeField] private float aimHeight = 1.2f; + [Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float aimHeight = 1.2f; // 변수를 선언할거에요 -> 조준 높이 오프셋(1.2)을 aimHeight에 - [Header("🏃‍♂️ 도망 설정")] - [SerializeField] private float fleeDistance = 5f; + [Header("🏃‍♂️ 도망 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 🏃‍♂️ 도망 설정 을 + [SerializeField] private float fleeDistance = 5f; // 변수를 선언할거에요 -> 도망가는 거리(5.0)를 fleeDistance에 - [Header("애니메이션 & 기타")] - [SerializeField] private float throwForce = 15f; // (정밀 모드 꺼졌을 때 사용) - [SerializeField] private float throwUpward = 5f; // (정밀 모드 꺼졌을 때 사용) - [SerializeField] private float reloadTime = 2.0f; - [SerializeField] private GameObject handModel; - [SerializeField] private bool aimAtPlayer = true; + [Header("애니메이션 & 기타")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 & 기타 를 + [SerializeField] private float throwForce = 15f; // 변수를 선언할거에요 -> 기본 투척 힘을 throwForce에 + [SerializeField] private float throwUpward = 5f; // 변수를 선언할거에요 -> 기본 상향 힘을 throwUpward에 + [SerializeField] private float reloadTime = 2.0f; // 변수를 선언할거에요 -> 장전 시간(2.0초)을 reloadTime에 + [SerializeField] private GameObject handModel; // 변수를 선언할거에요 -> 손에 든 무기 모델을 handModel에 + [SerializeField] private bool aimAtPlayer = true; // 변수를 선언할거에요 -> 플레이어 조준 여부를 aimAtPlayer에 - [SerializeField] private string attackAnim = "Monster_Attack"; - [SerializeField] private string walkAnim = "Monster_Walk"; - [SerializeField] private float patrolRadius = 5f; - [SerializeField] private float patrolInterval = 3f; + [SerializeField] private string attackAnim = "Monster_Attack"; // 변수를 선언할거에요 -> 공격 애니메이션 이름을 attackAnim에 + [SerializeField] private string walkAnim = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 walkAnim에 + [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 + [SerializeField] private float patrolInterval = 3f; // 변수를 선언할거에요 -> 순찰 간격(3.0초)을 patrolInterval에 - private float lastAttackTime; - private float nextPatrolTime; - // private bool isPlayerInZone; - private bool isReloading = false; + private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에 + private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 + private bool isReloading = false; // 변수를 선언할거에요 -> 장전 중 여부를 isReloading에 - protected override void Init() + protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을 { - if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; } - if (animator != null) animator.applyRootMotion = false; + if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; } // 조건이 맞으면 설정할거에요 -> 정지 거리를 사거리의 80%로, 속도를 3.5로 + if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로 } - protected override void ExecuteAILogic() + protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { - if (isHit || isAttacking || isResting || isReloading) return; - if (isAttacking && animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) OnAttackEnd(); + if (isHit || isAttacking || isResting || isReloading) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식, 장전 중이라면 + if (isAttacking && animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) OnAttackEnd(); // 조건이 맞으면 실행할거에요 -> 공격 중인데 모션이 대기라면 강제로 공격 종료를 - float dist = Vector3.Distance(transform.position, playerTransform.position); + float dist = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 - if (dist <= detectRange) HandleCombat(dist); - else + if (dist <= detectRange) HandleCombat(dist); // 조건이 맞으면 실행할거에요 -> 감지 거리 이내라면 전투 처리(HandleCombat)를 + else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 { - if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; } - Patrol(); - UpdateMovementAnimation(); + if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; } // 조건이 맞으면 실행할거에요 -> 추격 중이었다면 경로를 취소하고 순찰을 준비하기를 + Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을 + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 갱신을 } } - void HandleCombat(float dist) + void HandleCombat(float dist) // 함수를 선언할거에요 -> 거리별 전투 행동을 정하는 HandleCombat을 { - if (attackStyle == AttackStyle.Straight && dist < minDistance) RetreatFromPlayer(); - else if (dist <= attackRange) TryAttack(); - else { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); UpdateMovementAnimation(); } } + if (attackStyle == AttackStyle.Straight && dist < minDistance) RetreatFromPlayer(); // 조건이 맞으면 실행할거에요 -> 직선 공격 타입이고 너무 가까우면 도망가기를 + else if (dist <= attackRange) TryAttack(); // 조건이 맞으면 실행할거에요 -> 사거리 안이라면 공격 시도를 + else { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); UpdateMovementAnimation(); } } // 그 외엔 실행할거에요 -> 플레이어를 향해 추격하기를 } - void TryAttack() + void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을 { - if (Time.time < lastAttackTime + attackDelay) return; - lastAttackTime = Time.time; - isAttacking = true; + if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 쿨타임이 안 지났다면 + lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로 + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 - if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } + if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 이동을 멈추고 제자리에 서기를 - Vector3 lookDir = (playerTransform.position - transform.position).normalized; - lookDir.y = 0; - if (lookDir != Vector3.zero) transform.rotation = Quaternion.LookRotation(lookDir); + Vector3 lookDir = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 바라보는 방향을 + lookDir.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게 + if (lookDir != Vector3.zero) transform.rotation = Quaternion.LookRotation(lookDir); // 회전시킬거에요 -> 플레이어 쪽을 보도록 - animator.Play(attackAnim); + animator.Play(attackAnim); // 재생할거에요 -> 공격 애니메이션을 } - public override void OnAttackStart() + public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 타이밍(애니메이션 이벤트)에 호출될 OnAttackStart를 { - if (!projectilePrefab || !firePoint) return; + if (!projectilePrefab || !firePoint) return; // 조건이 맞으면 중단할거에요 -> 프리팹이나 발사 위치가 없다면 - GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation); + GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation); // 생성할거에요 -> 투사체를 발사 위치에 - if (obj.TryGetComponent(out var proj)) + if (obj.TryGetComponent(out var proj)) // 조건이 맞으면 실행할거에요 -> 투사체 스크립트가 있다면 { - float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f; - proj.Initialize(transform.forward, speed, attackDamage); + float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f; // 값을 결정할거에요 -> 직선이면 속도를, 곡사면 0(물리)을 + proj.Initialize(transform.forward, speed, attackDamage); // 초기화할거에요 -> 방향, 속도, 데미지 정보를 전달해서 } - if (attackStyle == AttackStyle.Lob) + if (attackStyle == AttackStyle.Lob) // 조건이 맞으면 실행할거에요 -> 곡사 공격 타입이라면 { - Rigidbody rb = obj.GetComponent(); - if (rb) + Rigidbody rb = obj.GetComponent(); // 컴포넌트를 가져올거에요 -> 투사체의 리지드바디를 + if (rb) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면 { - rb.useGravity = true; + rb.useGravity = true; // 기능을 켤거에요 -> 중력 영향을 받도록 // ⭐ 목표 지점 설정 (플레이어 위치 + 높이 보정) - Vector3 targetPos = playerTransform.position + Vector3.up * aimHeight; + Vector3 targetPos = playerTransform.position + Vector3.up * aimHeight; // 위치를 계산할거에요 -> 플레이어 위치에 조준 높이를 더해서 - if (usePreciseLob) + if (usePreciseLob) // 조건이 맞으면 실행할거에요 -> 정밀 곡사를 사용한다면 { // 보정된 위치(targetPos)를 기준으로 탄도 계산 - Vector3 velocity = CalculateLobVelocity(firePoint.position, targetPos, launchAngle); + Vector3 velocity = CalculateLobVelocity(firePoint.position, targetPos, launchAngle); // 계산할거에요 -> 목표에 도달하기 위한 물리 속도를 - if (!float.IsNaN(velocity.x)) + if (!float.IsNaN(velocity.x)) // 조건이 맞으면 실행할거에요 -> 계산 결과가 유효하다면 { - rb.velocity = velocity; + rb.velocity = velocity; // 값을 넣을거에요 -> 계산된 속도를 리지드바디에 } - else + else // 조건이 틀리면 실행할거에요 -> 계산 실패(도달 불가)라면 { // 계산 실패 시 백업 - rb.AddForce(transform.forward * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); + rb.AddForce(transform.forward * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 기본 힘으로 던지기를 } } - else + else // 조건이 틀리면 실행할거에요 -> 정밀 곡사가 아니라면 { - Vector3 dir = aimAtPlayer ? (targetPos - firePoint.position).normalized : transform.forward; - // dir.y = 0; // 높이 조준을 위해 y제거 주석 처리 (원하면 해제) - rb.AddForce(dir * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); + Vector3 dir = aimAtPlayer ? (targetPos - firePoint.position).normalized : transform.forward; // 방향을 결정할거에요 -> 타겟 방향 혹은 정면으로 + rb.AddForce(dir * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 방향과 힘을 적용해서 던지기를 } } - if (handModel != null) { handModel.SetActive(false); StartCoroutine(ReloadRoutine()); } + if (handModel != null) { handModel.SetActive(false); StartCoroutine(ReloadRoutine()); } // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 숨기고 장전 코루틴을 시작하기를 } } - Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle) + Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle) // 함수를 선언할거에요 -> 곡사 탄도 속도를 계산하는 CalculateLobVelocity를 { - Vector3 dir = target - origin; - float height = dir.y; - dir.y = 0; - float dist = dir.magnitude; + Vector3 dir = target - origin; // 벡터를 계산할거에요 -> 목표까지의 거리 벡터를 + float height = dir.y; // 값을 저장할거에요 -> 높이 차이를 + dir.y = 0; // 값을 바꿀거에요 -> 수평 거리 계산을 위해 y를 0으로 + float dist = dir.magnitude; // 값을 저장할거에요 -> 수평 거리를 - float a = angle * Mathf.Deg2Rad; + float a = angle * Mathf.Deg2Rad; // 값을 변환할거에요 -> 각도를 라디안으로 - dir.y = dist * Mathf.Tan(a); - dist += height / Mathf.Tan(a); + dir.y = dist * Mathf.Tan(a); // 값을 계산할거에요 -> 탄젠트를 이용해 높이를 + dist += height / Mathf.Tan(a); // 값을 보정할거에요 -> 높이 차이에 따른 거리 보정을 - float gravity = Physics.gravity.magnitude; - float velocitySq = (gravity * dist * dist) / (2 * Mathf.Pow(Mathf.Cos(a), 2) * (dist * Mathf.Tan(a) - height)); + float gravity = Physics.gravity.magnitude; // 값을 가져올거에요 -> 중력 가속도를 + // 물리 공식: v^2 = (g * x^2) / (2 * cos^2(a) * (x * tan(a) - y)) + float velocitySq = (gravity * dist * dist) / (2 * Mathf.Pow(Mathf.Cos(a), 2) * (dist * Mathf.Tan(a) - height)); // 값을 계산할거에요 -> 필요한 속도의 제곱을 - if (velocitySq <= 0) return Vector3.zero; + if (velocitySq <= 0) return Vector3.zero; // 조건이 맞으면 반환할거에요 -> 계산 불가라면 0 벡터를 - float velocity = Mathf.Sqrt(velocitySq); - return dir.normalized * velocity; + float velocity = Mathf.Sqrt(velocitySq); // 값을 계산할거에요 -> 제곱근을 구해서 실제 속도를 + return dir.normalized * velocity; // 반환할거에요 -> 방향 벡터에 속도를 곱해서 } - // ... (나머지 함수들은 기존과 동일) ... - void RetreatFromPlayer() + void RetreatFromPlayer() // 함수를 선언할거에요 -> 플레이어로부터 도망가는 RetreatFromPlayer를 { - if (agent.pathPending || (agent.remainingDistance > 0.5f && agent.velocity.magnitude > 0.1f)) { UpdateMovementAnimation(); return; } - Vector3 dir = (transform.position - playerTransform.position).normalized; - Vector3 pos = transform.position + dir * fleeDistance; - if (NavMesh.SamplePosition(pos, out NavMeshHit hit, 5f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } - UpdateMovementAnimation(); + if (agent.pathPending || (agent.remainingDistance > 0.5f && agent.velocity.magnitude > 0.1f)) { UpdateMovementAnimation(); return; } // 조건이 맞으면 중단할거에요 -> 이미 이동 중이라면 + Vector3 dir = (transform.position - playerTransform.position).normalized; // 벡터를 계산할거에요 -> 플레이어 반대 방향을 + Vector3 pos = transform.position + dir * fleeDistance; // 위치를 계산할거에요 -> 도망갈 목표 지점을 + if (NavMesh.SamplePosition(pos, out NavMeshHit hit, 5f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 그곳으로 이동하기를 + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션 갱신을 } - IEnumerator ReloadRoutine() + IEnumerator ReloadRoutine() // 코루틴 함수를 선언할거에요 -> 무기 장전(재생성)을 처리하는 ReloadRoutine을 { - isReloading = true; - yield return new WaitForSeconds(reloadTime); - if (handModel != null) handModel.SetActive(true); - isReloading = false; + isReloading = true; // 상태를 바꿀거에요 -> 장전 중 상태를 참으로 + yield return new WaitForSeconds(reloadTime); // 기다릴거에요 -> 장전 시간만큼 + if (handModel != null) handModel.SetActive(true); // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 다시 보이게 + isReloading = false; // 상태를 바꿀거에요 -> 장전 중 상태를 거짓으로 } - public override void OnAttackEnd() + public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를 { - isAttacking = false; - if (animator != null) animator.Play(Monster_Idle); - if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); + isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓으로 + if (animator != null) animator.Play(Monster_Idle); // 조건이 맞으면 실행할거에요 -> 대기 애니메이션으로 복귀를 + if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있다면 휴식 코루틴 시작을 } - void Patrol() + void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을 { - if (Time.time < nextPatrolTime) { if (agent.velocity.magnitude < 0.1f) animator.Play(Monster_Idle); return; } - Vector3 randomPoint = transform.position + new Vector3(Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius)); - if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } - nextPatrolTime = Time.time + patrolInterval; + if (Time.time < nextPatrolTime) { if (agent.velocity.magnitude < 0.1f) animator.Play(Monster_Idle); return; } // 조건이 맞으면 중단할거에요 -> 대기 시간이라면 대기 모션 재생 후 리턴 + Vector3 randomPoint = transform.position + new Vector3(Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius)); // 벡터를 계산할거에요 -> 랜덤 순찰 지점을 + if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 이동하기를 + nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을 } - void UpdateMovementAnimation() + void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션 갱신 함수를 { - if (isAttacking || isHit || isResting) return; - if (agent.velocity.magnitude > 0.1f) animator.Play(walkAnim); else animator.Play(Monster_Idle); + if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 + if (agent.velocity.magnitude > 0.1f) animator.Play(walkAnim); else animator.Play(Monster_Idle); // 조건에 따라 재생할거에요 -> 걷기 또는 대기 애니메이션을 } - - // private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } - //private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/Rangedbase.cs b/Assets/Scripts/Enemy/AI/Rangedbase.cs index 96e58059..d10ac71a 100644 --- a/Assets/Scripts/Enemy/AI/Rangedbase.cs +++ b/Assets/Scripts/Enemy/AI/Rangedbase.cs @@ -1,25 +1,25 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 /// /// 만능 투사체 (화살, 마법, 돌맹이 공용) /// -public class Projectile : MonoBehaviour +public class Projectile : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 Projectile을 { - [Header("기본 설정")] - [SerializeField] private float lifetime = 5f; // 수명 (5초 뒤 삭제) - [SerializeField] private GameObject hitEffectPrefab; // 맞으면 나오는 이펙트 (폭발 등) - [SerializeField] private bool destroyOnHit = true; // 맞으면 사라짐? (관통 아니면 true) + [Header("기본 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 기본 설정 을 + [SerializeField] private float lifetime = 5f; // 변수를 선언할거에요 -> 수명(5.0초)을 lifetime에 + [SerializeField] private GameObject hitEffectPrefab; // 변수를 선언할거에요 -> 충돌 이펙트 프리팹을 hitEffectPrefab에 + [SerializeField] private bool destroyOnHit = true; // 변수를 선언할거에요 -> 충돌 시 삭제 여부를 destroyOnHit에 // 내부 변수 - private Vector3 _direction; - private float _speed; - private float _damage; - private bool _isInitialized = false; - private Rigidbody _rigidbody; + private Vector3 _direction; // 변수를 선언할거에요 -> 날아갈 방향을 _direction에 + private float _speed; // 변수를 선언할거에요 -> 이동 속도를 _speed에 + private float _damage; // 변수를 선언할거에요 -> 공격 데미지를 _damage에 + private bool _isInitialized = false; // 변수를 초기화할거에요 -> 초기화 여부를 거짓(false)으로 + private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트 _rigidbody를 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를 { - _rigidbody = GetComponent(); + _rigidbody = GetComponent(); // 컴포넌트를 가져올거에요 -> 리지드바디를 } /// @@ -28,79 +28,79 @@ public class Projectile : MonoBehaviour /// 날아갈 방향 /// 속도 (0이면 물리력으로 날아감) /// 줄 데미지 - public void Initialize(Vector3 direction, float speed, float damage) + public void Initialize(Vector3 direction, float speed, float damage) // 함수를 선언할거에요 -> 투사체 정보를 설정하는 Initialize를 { - _direction = direction.normalized; - _speed = speed; - _damage = damage; - _isInitialized = true; + _direction = direction.normalized; // 값을 저장할거에요 -> 방향 벡터를 정규화해서 + _speed = speed; // 값을 저장할거에요 -> 속도를 + _damage = damage; // 값을 저장할거에요 -> 데미지를 + _isInitialized = true; // 상태를 바꿀거에요 -> 초기화 완료 상태를 참으로 // 속도가 있다? -> 화살/마법 (직선 운동) - if (_rigidbody != null && speed > 0) + if (_rigidbody != null && speed > 0) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있고 속도가 있다면 { - _rigidbody.velocity = _direction * _speed; - _rigidbody.useGravity = false; // 마법은 보통 중력 무시 + _rigidbody.velocity = _direction * _speed; // 값을 넣을거에요 -> 속도 벡터를 리지드바디 속도에 + _rigidbody.useGravity = false; // 설정을 바꿀거에요 -> 중력 영향을 끄기로 (직선 비행) } // 속도가 0이다? -> 돌맹이 (외부에서 물리력 가함) - else if (_rigidbody != null && speed == 0) + else if (_rigidbody != null && speed == 0) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있고 속도가 0이라면 { - _rigidbody.useGravity = true; // 돌은 중력 받아야 함 + _rigidbody.useGravity = true; // 설정을 바꿀거에요 -> 중력 영향을 켜기로 (포물선 비행) } // 수명 지나면 자동 삭제 - Destroy(gameObject, lifetime); + Destroy(gameObject, lifetime); // 파괴할거에요 -> 이 오브젝트를 수명 시간이 지나면 } - private void FixedUpdate() + private void FixedUpdate() // 함수를 실행할거에요 -> 물리 연산 주기로 호출되는 FixedUpdate를 { - if (!_isInitialized) return; + if (!_isInitialized) return; // 조건이 맞으면 중단할거에요 -> 초기화되지 않았다면 // 리지드바디가 없거나, Kinematic인 경우 (강제 이동 보정) - if (_rigidbody == null || _rigidbody.isKinematic) + if (_rigidbody == null || _rigidbody.isKinematic) // 조건이 맞으면 실행할거에요 -> 물리 이동을 안 쓰는 상태라면 { - if (_speed > 0) + if (_speed > 0) // 조건이 맞으면 실행할거에요 -> 속도가 설정되어 있다면 { - transform.position += _direction * _speed * Time.fixedDeltaTime; + transform.position += _direction * _speed * Time.fixedDeltaTime; // 이동시킬거에요 -> 방향과 속도에 맞춰 직접 위치를 } } } - private void OnTriggerEnter(Collider other) + private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 충돌이 발생했을 때 OnTriggerEnter를 { // 1. 적(몬스터)끼리 맞으면 무시 - if (other.CompareTag("Enemy") || other.gameObject == gameObject) return; + if (other.CompareTag("Enemy") || other.gameObject == gameObject) return; // 조건이 맞으면 중단할거에요 -> 적끼리 충돌했거나 자기 자신이라면 // 2. 투사체끼리 충돌 무시 - if (other.GetComponent()) return; + if (other.GetComponent()) return; // 조건이 맞으면 중단할거에요 -> 다른 투사체와 충돌했다면 // 3. 플레이어 피격 처리 - if (other.CompareTag("Player")) + if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌 대상이 플레이어라면 { // PlayerHealth 혹은 IDamageable 스크립트 찾기 - var playerHealth = other.GetComponent(); + var playerHealth = other.GetComponent(); // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를 - if (playerHealth != null) + if (playerHealth != null) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 { // 무적 상태 체크 (있다면) - if (!playerHealth.isInvincible) + if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 플레이어가 무적 상태가 아니라면 { - playerHealth.TakeDamage(_damage); - Debug.Log($"🎯 [적중] 플레이어에게 {_damage} 데미지!"); + playerHealth.TakeDamage(_damage); // 함수를 실행할거에요 -> 데미지를 입히는 TakeDamage를 + Debug.Log($"🎯 [적중] 플레이어에게 {_damage} 데미지!"); // 로그를 출력할거에요 -> 적중 메시지를 } } } // 4. 벽이나 땅에 닿았을 때도 이펙트 - if (other.CompareTag("Ground") || other.CompareTag("Wall") || other.CompareTag("Player")) + if (other.CompareTag("Ground") || other.CompareTag("Wall") || other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 땅, 벽, 플레이어에 닿았다면 { - if (hitEffectPrefab != null) + if (hitEffectPrefab != null) // 조건이 맞으면 실행할거에요 -> 충돌 이펙트 프리팹이 있다면 { - Instantiate(hitEffectPrefab, transform.position, Quaternion.identity); + Instantiate(hitEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 이펙트를 충돌 위치에 } - if (destroyOnHit) + if (destroyOnHit) // 조건이 맞으면 실행할거에요 -> 충돌 시 삭제 옵션이 켜져 있다면 { - Destroy(gameObject); + Destroy(gameObject); // 파괴할거에요 -> 이 투사체 오브젝트를 } } } diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs b/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs index fdd6bd33..b951280d 100644 --- a/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs +++ b/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs @@ -1,77 +1,75 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 /// /// 보스 카운터 시스템의 임계치, 가중치, 감소(Decay) 설정을 담는 ScriptableObject. /// Inspector에서 밸런싱을 위해 쉽게 조정할 수 있습니다. -/// -/// [사용법] Project 창에서 우클릭 → Create → Boss/Counter Config /// -[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/Counter Config")] -public class BossCounterConfig : ScriptableObject +[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/Counter Config")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "Boss/Counter Config"를 +public class BossCounterConfig : ScriptableObject // 클래스를 선언할거에요 -> ScriptableObject를 상속받는 BossCounterConfig를 { - [Header("══ 기본 임계치 (첫 런 / 잠금 해제 전) ══")] - [Tooltip("회피 카운터 발동 조건: 10초 내 회피 횟수")] - public int dodgeThreshold = 5; + [Header("══ 기본 임계치 (첫 런 / 잠금 해제 전) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 기본 임계치 (첫 런 / 잠금 해제 전) ══ 을 + [Tooltip("회피 카운터 발동 조건: 10초 내 회피 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public int dodgeThreshold = 5; // 변수를 선언할거에요 -> 회피 횟수 임계값(5)을 dodgeThreshold에 - [Tooltip("조준 카운터 발동 조건: 10초 내 조준 유지 시간(초)")] - public float aimThreshold = 4.0f; + [Tooltip("조준 카운터 발동 조건: 10초 내 조준 유지 시간(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public float aimThreshold = 4.0f; // 변수를 선언할거에요 -> 조준 시간 임계값(4.0초)을 aimThreshold에 - [Tooltip("관통 카운터 발동 조건: 10초 내 관통 비율")] - [Range(0f, 1f)] - public float pierceThreshold = 0.6f; + [Tooltip("관통 카운터 발동 조건: 10초 내 관통 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0f, 1f)] // 슬라이더를 표시할거에요 -> 0부터 1 사이로 + public float pierceThreshold = 0.6f; // 변수를 선언할거에요 -> 관통 비율 임계값(60%)을 pierceThreshold에 - [Tooltip("관통 비율 판단을 위한 최소 발사 횟수")] - public int minShotsForPierceCheck = 3; + [Tooltip("관통 비율 판단을 위한 최소 발사 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public int minShotsForPierceCheck = 3; // 변수를 선언할거에요 -> 최소 발사 횟수(3)를 minShotsForPierceCheck에 - [Header("══ 잠금 해제 후 임계치 감소 ══")] - [Tooltip("잠금 해제된 카운터의 임계치 감소 비율 (0.8 = 20% 낮아짐)")] - [Range(0.5f, 1.0f)] - public float unlockedThresholdMultiplier = 0.8f; + [Header("══ 잠금 해제 후 임계치 감소 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 잠금 해제 후 임계치 감소 ══ 를 + [Tooltip("잠금 해제된 카운터의 임계치 감소 비율 (0.8 = 20% 낮아짐)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0.5f, 1.0f)] // 슬라이더를 표시할거에요 -> 0.5부터 1.0 사이로 + public float unlockedThresholdMultiplier = 0.8f; // 변수를 선언할거에요 -> 잠금 해제 시 감소 배율(0.8)을 unlockedThresholdMultiplier에 - [Header("══ 가중치 설정 (확률 가중치 방식) ══")] - [Tooltip("카운터 발동 시 해당 카운터 패턴에 추가되는 가중치")] - public float counterWeightBonus = 3f; + [Header("══ 가중치 설정 (확률 가중치 방식) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 가중치 설정 (확률 가중치 방식) ══ 을 + [Tooltip("카운터 발동 시 해당 카운터 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public float counterWeightBonus = 3f; // 변수를 선언할거에요 -> 카운터 패턴 보너스 가중치(3.0)를 counterWeightBonus에 - [Tooltip("카운터 발동 시 보조 패턴에 추가되는 가중치")] - public float counterSubWeightBonus = 2f; + [Tooltip("카운터 발동 시 보조 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public float counterSubWeightBonus = 2f; // 변수를 선언할거에요 -> 보조 패턴 보너스 가중치(2.0)를 counterSubWeightBonus에 - [Header("══ Decay(감소) 설정 ══")] - [Tooltip("카운터 모드 유지 시간(초). 이 시간 동안 조건 미충족 시 비활성화")] - public float counterDecayTime = 8f; + [Header("══ Decay(감소) 설정 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ Decay(감소) 설정 ══ 을 + [Tooltip("카운터 모드 유지 시간(초). 이 시간 동안 조건 미충족 시 비활성화")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public float counterDecayTime = 8f; // 변수를 선언할거에요 -> 카운터 유지 시간(8.0초)을 counterDecayTime에 - [Header("══ 빈도 제한 ══")] - [Tooltip("카운터 패턴 선택 확률 상한 (0.25 = 최대 25%)")] - [Range(0.1f, 0.5f)] - public float maxCounterFrequency = 0.25f; + [Header("══ 빈도 제한 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 빈도 제한 ══ 을 + [Tooltip("카운터 패턴 선택 확률 상한 (0.25 = 최대 25%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0.1f, 0.5f)] // 슬라이더를 표시할거에요 -> 0.1부터 0.5 사이로 + public float maxCounterFrequency = 0.25f; // 변수를 선언할거에요 -> 카운터 최대 빈도(25%)를 maxCounterFrequency에 - [Tooltip("카운터 패턴 발동 후 쿨타임(초)")] - public float counterCooldown = 5f; + [Tooltip("카운터 패턴 발동 후 쿨타임(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public float counterCooldown = 5f; // 변수를 선언할거에요 -> 카운터 재사용 대기시간(5.0초)을 counterCooldown에 - [Header("══ 습관 변경 보상 ══")] - [Tooltip("플레이어가 이전 런에서 발동된 카운터 습관을 이번 런에서 안 보이면, 보스 일반 패턴 난이도 감소 비율")] - [Range(0f, 0.3f)] - public float habitChangeRewardRatio = 0.15f; + [Header("══ 습관 변경 보상 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 습관 변경 보상 ══ 을 + [Tooltip("플레이어가 이전 런에서 발동된 카운터 습관을 이번 런에서 안 보이면, 보스 일반 패턴 난이도 감소 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [Range(0f, 0.3f)] // 슬라이더를 표시할거에요 -> 0부터 0.3 사이로 + public float habitChangeRewardRatio = 0.15f; // 변수를 선언할거에요 -> 습관 변경 보상 비율(15%)을 habitChangeRewardRatio에 // ═══════════════════════════════════════════ // 임계치 조회 (잠금 해제 여부 반영) // ═══════════════════════════════════════════ /// 현재 유효 임계치 반환 (잠금 해제 시 낮아짐) - public int GetEffectiveDodgeThreshold(bool isUnlocked) + public int GetEffectiveDodgeThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 회피 임계값을 반환하는 GetEffectiveDodgeThreshold를 { - if (!isUnlocked) return dodgeThreshold; - return Mathf.Max(2, Mathf.RoundToInt(dodgeThreshold * unlockedThresholdMultiplier)); + if (!isUnlocked) return dodgeThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을 + return Mathf.Max(2, Mathf.RoundToInt(dodgeThreshold * unlockedThresholdMultiplier)); // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱하고 최소 2 이상이 되도록 } - public float GetEffectiveAimThreshold(bool isUnlocked) + public float GetEffectiveAimThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 조준 임계값을 반환하는 GetEffectiveAimThreshold를 { - if (!isUnlocked) return aimThreshold; - return aimThreshold * unlockedThresholdMultiplier; + if (!isUnlocked) return aimThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을 + return aimThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을 } - public float GetEffectivePierceThreshold(bool isUnlocked) + public float GetEffectivePierceThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 관통 임계값을 반환하는 GetEffectivePierceThreshold를 { - if (!isUnlocked) return pierceThreshold; - return pierceThreshold * unlockedThresholdMultiplier; + if (!isUnlocked) return pierceThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을 + return pierceThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을 } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs b/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs index ca6f380f..f4c8088f 100644 --- a/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs +++ b/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs @@ -1,48 +1,49 @@ -using System.Collections; -using UnityEngine; -using UnityEngine.UI; -using TMPro; +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를 +using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를 /// /// 보스 카운터 시스템의 연출/UI를 담당합니다. -/// 카운터 발동 시 보스 대사, 화면 효과 등을 처리합니다. -/// -/// "...또 그 수법이냐" 같은 대사로 "보스가 기억한다"는 서사 전달. /// -public class BossCounterFeedback : MonoBehaviour +public class BossCounterFeedback : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterFeedback을 { - [Header("참조")] - [SerializeField] private BossCounterSystem counterSystem; + [Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를 + [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 카운터 시스템 스크립트를 연결할 counterSystem을 - [Header("UI 요소 (선택)")] - [SerializeField] private TextMeshProUGUI bossDialogueText; - [SerializeField] private CanvasGroup dialogueCanvasGroup; - [SerializeField] private float dialogueDisplayDuration = 3f; - [SerializeField] private float dialogueFadeDuration = 0.5f; + [Header("UI 요소 (선택)")] // 인스펙터 창에 제목을 표시할거에요 -> UI 요소 (선택) 을 + [SerializeField] private TextMeshProUGUI bossDialogueText; // 변수를 선언할거에요 -> 보스 대사를 표시할 텍스트 UI를 + [SerializeField] private CanvasGroup dialogueCanvasGroup; // 변수를 선언할거에요 -> 대사창의 투명도를 조절할 캔버스 그룹을 + [SerializeField] private float dialogueDisplayDuration = 3f; // 변수를 선언할거에요 -> 대사 표시 시간(3초)을 dialogueDisplayDuration에 + [SerializeField] private float dialogueFadeDuration = 0.5f; // 변수를 선언할거에요 -> 대사 페이드 시간(0.5초)을 dialogueFadeDuration에 - [Header("카운터별 보스 대사")] - [SerializeField] private string[] dodgeCounterDialogues = new string[] + [Header("카운터별 보스 대사")] // 인스펙터 창에 제목을 표시할거에요 -> 카운터별 보스 대사 를 + [SerializeField] + private string[] dodgeCounterDialogues = new string[] // 배열을 초기화할거에요 -> 회피 카운터 대사 목록을 { "...또 도망치려는 건가.", "네 발은 이미 읽었다.", "아무리 피해도 소용없어." }; - [SerializeField] private string[] aimCounterDialogues = new string[] + [SerializeField] + private string[] aimCounterDialogues = new string[] // 배열을 초기화할거에요 -> 조준 카운터 대사 목록을 { "그렇게 오래 노려봐야...", "느린 조준은 빈틈이지.", "시간을 줄 생각은 없다." }; - [SerializeField] private string[] pierceCounterDialogues = new string[] + [SerializeField] + private string[] pierceCounterDialogues = new string[] // 배열을 초기화할거에요 -> 관통 카운터 대사 목록을 { "그 화살은 더 이상 통하지 않아.", "관통? 이번엔 막아주지.", "같은 수를 반복하다니." }; - [SerializeField] private string[] habitChangeDialogues = new string[] + [SerializeField] + private string[] habitChangeDialogues = new string[] // 배열을 초기화할거에요 -> 습관 변경 보상 대사 목록을 { "...다른 수를 쓰는 건가.", "흥, 조금은 배운 모양이군.", @@ -50,33 +51,34 @@ public class BossCounterFeedback : MonoBehaviour }; // ── 잠금 해제 여부에 따른 첫 발동 대사 ── - [Header("첫 잠금 해제 시 대사 (서사 강조)")] - [SerializeField] private string[] firstUnlockDialogues = new string[] + [Header("첫 잠금 해제 시 대사 (서사 강조)")] // 인스펙터 창에 제목을 표시할거에요 -> 첫 잠금 해제 시 대사 (서사 강조) 를 + [SerializeField] + private string[] firstUnlockDialogues = new string[] // 배열을 초기화할거에요 -> 첫 해금 시 출력할 대사 목록을 { "...기억했다. 네 버릇을.", "이제 알겠어. 네 전투 방식이.", "한 번이면 충분해. 패턴을 읽었다." }; - private Coroutine dialogueCoroutine; + private Coroutine dialogueCoroutine; // 변수를 선언할거에요 -> 실행 중인 대사 코루틴을 저장할 dialogueCoroutine을 - private void OnEnable() + private void OnEnable() // 함수를 실행할거에요 -> 오브젝트 활성화 시 호출되는 OnEnable을 { - if (counterSystem != null) + if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면 { - counterSystem.OnCounterActivated.AddListener(OnCounterActivated); - counterSystem.OnCounterDeactivated.AddListener(OnCounterDeactivated); - counterSystem.OnHabitChangeRewarded.AddListener(OnHabitChangeRewarded); + counterSystem.OnCounterActivated.AddListener(OnCounterActivated); // 구독할거에요 -> 카운터 활성 이벤트에 OnCounterActivated를 + counterSystem.OnCounterDeactivated.AddListener(OnCounterDeactivated); // 구독할거에요 -> 카운터 비활성 이벤트에 OnCounterDeactivated를 + counterSystem.OnHabitChangeRewarded.AddListener(OnHabitChangeRewarded); // 구독할거에요 -> 보상 이벤트에 OnHabitChangeRewarded를 } } - private void OnDisable() + private void OnDisable() // 함수를 실행할거에요 -> 오브젝트 비활성화 시 호출되는 OnDisable을 { - if (counterSystem != null) + if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면 { - counterSystem.OnCounterActivated.RemoveListener(OnCounterActivated); - counterSystem.OnCounterDeactivated.RemoveListener(OnCounterDeactivated); - counterSystem.OnHabitChangeRewarded.RemoveListener(OnHabitChangeRewarded); + counterSystem.OnCounterActivated.RemoveListener(OnCounterActivated); // 구독 해제할거에요 -> 카운터 활성 이벤트를 + counterSystem.OnCounterDeactivated.RemoveListener(OnCounterDeactivated); // 구독 해제할거에요 -> 카운터 비활성 이벤트를 + counterSystem.OnHabitChangeRewarded.RemoveListener(OnHabitChangeRewarded); // 구독 해제할거에요 -> 보상 이벤트를 } } @@ -84,106 +86,100 @@ public class BossCounterFeedback : MonoBehaviour // 이벤트 핸들러 // ═══════════════════════════════════════════ - private void OnCounterActivated(CounterType type) + private void OnCounterActivated(CounterType type) // 함수를 선언할거에요 -> 카운터 활성 시 호출될 OnCounterActivated를 { - var persistence = BossCounterPersistence.Instance; + var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에 // 첫 잠금 해제인지 확인 (총 발동 횟수가 1이면 방금 처음 잠금 해제된 것) - bool isFirstUnlock = false; - if (persistence != null) + bool isFirstUnlock = false; // 변수를 초기화할거에요 -> 첫 해금 여부를 거짓(false)으로 + if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면 { - int activations = type switch + int activations = type switch // 값을 가져올거에요 -> 타입에 따른 발동 횟수를 { CounterType.Dodge => persistence.Data.dodgeCounterActivations, CounterType.Aim => persistence.Data.aimCounterActivations, CounterType.Pierce => persistence.Data.pierceCounterActivations, _ => 0 }; - isFirstUnlock = (activations <= 1); + isFirstUnlock = (activations <= 1); // 판단할거에요 -> 발동 횟수가 1 이하라면 첫 해금이라고 } // 대사 선택 - string dialogue; - if (isFirstUnlock) + string dialogue; // 변수를 선언할거에요 -> 출력할 대사를 담을 dialogue를 + if (isFirstUnlock) // 조건이 맞으면 실행할거에요 -> 첫 해금이라면 { - dialogue = firstUnlockDialogues[Random.Range(0, firstUnlockDialogues.Length)]; + dialogue = firstUnlockDialogues[Random.Range(0, firstUnlockDialogues.Length)]; // 선택할거에요 -> 첫 해금 대사 중 하나를 랜덤으로 } - else + else // 조건이 틀리면 실행할거에요 -> 이미 해금된 상태라면 { - dialogue = type switch + dialogue = type switch // 분기할거에요 -> 카운터 타입에 따라 { - CounterType.Dodge => dodgeCounterDialogues[Random.Range(0, dodgeCounterDialogues.Length)], - CounterType.Aim => aimCounterDialogues[Random.Range(0, aimCounterDialogues.Length)], - CounterType.Pierce => pierceCounterDialogues[Random.Range(0, pierceCounterDialogues.Length)], - _ => "" + CounterType.Dodge => dodgeCounterDialogues[Random.Range(0, dodgeCounterDialogues.Length)], // 선택할거에요 -> 회피 대사를 + CounterType.Aim => aimCounterDialogues[Random.Range(0, aimCounterDialogues.Length)], // 선택할거에요 -> 조준 대사를 + CounterType.Pierce => pierceCounterDialogues[Random.Range(0, pierceCounterDialogues.Length)], // 선택할거에요 -> 관통 대사를 + _ => "" // 기본값은 빈 문자열로 }; } - ShowDialogue(dialogue); + ShowDialogue(dialogue); // 함수를 실행할거에요 -> 선택된 대사를 화면에 띄우는 ShowDialogue를 - // TODO: 여기에 추가 연출 넣기 - // - 보스 전용 애니메이션 트리거 (예: animator.SetTrigger("CounterReady")) - // - 보스 주변 이펙트 (파티클, 오라 등) - // - 카메라 연출 (줌인, 슬로모션 등) - // - 사운드 효과 - Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 재생" + - (isFirstUnlock ? " (첫 잠금 해제!)" : "")); + Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 재생" + (isFirstUnlock ? " (첫 잠금 해제!)" : "")); // 로그를 출력할거에요 -> 연출 재생 알림을 } - private void OnCounterDeactivated(CounterType type) + private void OnCounterDeactivated(CounterType type) // 함수를 선언할거에요 -> 카운터 종료 시 호출될 OnCounterDeactivated를 { // 카운터 해제 시 연출 (이펙트 종료 등) - Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 종료"); + Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 종료"); // 로그를 출력할거에요 -> 연출 종료 알림을 } - private void OnHabitChangeRewarded() + private void OnHabitChangeRewarded() // 함수를 선언할거에요 -> 보상 지급 시 호출될 OnHabitChangeRewarded를 { - string dialogue = habitChangeDialogues[Random.Range(0, habitChangeDialogues.Length)]; - ShowDialogue(dialogue); + string dialogue = habitChangeDialogues[Random.Range(0, habitChangeDialogues.Length)]; // 선택할거에요 -> 보상 대사 중 하나를 랜덤으로 + ShowDialogue(dialogue); // 함수를 실행할거에요 -> 대사를 띄우는 ShowDialogue를 - Debug.Log("[BossCounterFeedback] 습관 변경 보상 연출!"); + Debug.Log("[BossCounterFeedback] 습관 변경 보상 연출!"); // 로그를 출력할거에요 -> 보상 연출 알림을 } // ═══════════════════════════════════════════ // 대사 표시 // ═══════════════════════════════════════════ - private void ShowDialogue(string text) + private void ShowDialogue(string text) // 함수를 선언할거에요 -> 텍스트를 UI에 표시하는 ShowDialogue를 { - if (bossDialogueText == null || dialogueCanvasGroup == null) return; + if (bossDialogueText == null || dialogueCanvasGroup == null) return; // 조건이 맞으면 중단할거에요 -> UI 요소가 연결되지 않았다면 - if (dialogueCoroutine != null) - StopCoroutine(dialogueCoroutine); + if (dialogueCoroutine != null) // 조건이 맞으면 실행할거에요 -> 이미 실행 중인 대사가 있다면 + StopCoroutine(dialogueCoroutine); // 중단할거에요 -> 기존 코루틴을 - dialogueCoroutine = StartCoroutine(DialogueRoutine(text)); + dialogueCoroutine = StartCoroutine(DialogueRoutine(text)); // 코루틴을 시작할거에요 -> 새 대사를 출력하는 DialogueRoutine을 } - private IEnumerator DialogueRoutine(string text) + private IEnumerator DialogueRoutine(string text) // 코루틴 함수를 선언할거에요 -> 페이드 효과와 함께 대사를 출력하는 DialogueRoutine을 { - bossDialogueText.text = text; - dialogueCanvasGroup.alpha = 0f; + bossDialogueText.text = text; // 값을 설정할거에요 -> UI 텍스트 내용을 전달받은 text로 + dialogueCanvasGroup.alpha = 0f; // 값을 설정할거에요 -> 투명도를 0(안 보임)으로 // 페이드 인 - float t = 0f; - while (t < dialogueFadeDuration) + float t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로 + while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안 { - t += Time.deltaTime; - dialogueCanvasGroup.alpha = t / dialogueFadeDuration; - yield return null; + t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을 + dialogueCanvasGroup.alpha = t / dialogueFadeDuration; // 값을 조절할거에요 -> 투명도를 서서히 1로 + yield return null; // 대기할거에요 -> 다음 프레임까지 } - dialogueCanvasGroup.alpha = 1f; + dialogueCanvasGroup.alpha = 1f; // 값을 확정할거에요 -> 투명도를 완전 불투명(1)으로 // 유지 - yield return new WaitForSeconds(dialogueDisplayDuration); + yield return new WaitForSeconds(dialogueDisplayDuration); // 기다릴거에요 -> 대사 유지 시간만큼 // 페이드 아웃 - t = 0f; - while (t < dialogueFadeDuration) + t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 다시 0으로 + while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안 { - t += Time.deltaTime; - dialogueCanvasGroup.alpha = 1f - (t / dialogueFadeDuration); - yield return null; + t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을 + dialogueCanvasGroup.alpha = 1f - (t / dialogueFadeDuration); // 값을 조절할거에요 -> 투명도를 서서히 0으로 + yield return null; // 대기할거에요 -> 다음 프레임까지 } - dialogueCanvasGroup.alpha = 0f; + dialogueCanvasGroup.alpha = 0f; // 값을 확정할거에요 -> 투명도를 완전 투명(0)으로 } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs b/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs index d75ca940..80aeed41 100644 --- a/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs +++ b/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs @@ -1,50 +1,40 @@ -using System.Collections.Generic; -using UnityEngine; +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 /// /// 런 간 영구 저장되는 보스 카운터 잠금 해제 데이터. -/// "보스가 플레이어를 기억한다"는 서사를 담당합니다. -/// -/// 핵심 규칙: -/// - 잠금 해제는 영구 (런이 끝나도 유지) -/// - 발동은 조건부 (해당 런에서 플레이어가 습관을 보여야만 활성화) -/// - 잠금 해제된 카운터는 임계치가 약간 낮아짐 (보스가 "이미 알고 있으니 더 빨리 눈치챔") /// -[System.Serializable] -public class BossCounterSaveData +[System.Serializable] // 직렬화할거에요 -> 인스펙터나 JSON으로 저장 가능하게 +public class BossCounterSaveData // 클래스를 선언할거에요 -> 저장할 데이터 구조체인 BossCounterSaveData를 { - public bool dodgeCounterUnlocked = false; - public bool aimCounterUnlocked = false; - public bool pierceCounterUnlocked = false; + public bool dodgeCounterUnlocked = false; // 변수를 선언할거에요 -> 회피 카운터 해금 여부를 + public bool aimCounterUnlocked = false; // 변수를 선언할거에요 -> 조준 카운터 해금 여부를 + public bool pierceCounterUnlocked = false; // 변수를 선언할거에요 -> 관통 카운터 해금 여부를 - /// 각 카운터가 발동된 총 횟수 (연출/난이도 스케일링에 활용 가능) - public int dodgeCounterActivations = 0; - public int aimCounterActivations = 0; - public int pierceCounterActivations = 0; + /// 각 카운터가 발동된 총 횟수 + public int dodgeCounterActivations = 0; // 변수를 선언할거에요 -> 회피 카운터 총 발동 횟수를 + public int aimCounterActivations = 0; // 변수를 선언할거에요 -> 조준 카운터 총 발동 횟수를 + public int pierceCounterActivations = 0; // 변수를 선언할거에요 -> 관통 카운터 총 발동 횟수를 } -/// -/// 보스 카운터 영구 데이터를 관리합니다. -/// PlayerPrefs 기반으로 런 간 저장/로드합니다. -/// -public class BossCounterPersistence : MonoBehaviour +public class BossCounterPersistence : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterPersistence를 { - public static BossCounterPersistence Instance { get; private set; } + public static BossCounterPersistence Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를 - private const string SAVE_KEY = "BossCounterData"; + private const string SAVE_KEY = "BossCounterData"; // 상수를 정의할거에요 -> 저장에 사용할 키 이름("BossCounterData")을 SAVE_KEY에 - public BossCounterSaveData Data { get; private set; } + public BossCounterSaveData Data { get; private set; } // 프로퍼티를 선언할거에요 -> 실제 저장 데이터를 담을 Data를 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를 { - if (Instance != null && Instance != this) + if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면 { - Destroy(gameObject); - return; + Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를 + return; // 중단할거에요 -> 초기화 로직을 } - Instance = this; - DontDestroyOnLoad(gameObject); - Load(); + Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에 + DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬이 바뀌어도 이 오브젝트를 파괴하지 않게 + Load(); // 함수를 실행할거에요 -> 저장된 데이터를 불러오는 Load를 } // ═══════════════════════════════════════════ @@ -52,88 +42,88 @@ public class BossCounterPersistence : MonoBehaviour // ═══════════════════════════════════════════ /// 특정 카운터 타입을 영구 잠금 해제 - public void UnlockCounter(CounterType type) + public void UnlockCounter(CounterType type) // 함수를 선언할거에요 -> 카운터를 해금하는 UnlockCounter를 { - switch (type) + switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라 { - case CounterType.Dodge: - if (!Data.dodgeCounterUnlocked) + case CounterType.Dodge: // 조건이 맞으면 실행할거에요 -> 회피 타입이라면 + if (!Data.dodgeCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면 { - Data.dodgeCounterUnlocked = true; - Debug.Log("[BossCounter] 회피 카운터 영구 잠금 해제!"); + Data.dodgeCounterUnlocked = true; // 값을 바꿀거에요 -> 회피 해금 상태를 참(true)으로 + Debug.Log("[BossCounter] 회피 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를 } break; - case CounterType.Aim: - if (!Data.aimCounterUnlocked) + case CounterType.Aim: // 조건이 맞으면 실행할거에요 -> 조준 타입이라면 + if (!Data.aimCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면 { - Data.aimCounterUnlocked = true; - Debug.Log("[BossCounter] 조준 카운터 영구 잠금 해제!"); + Data.aimCounterUnlocked = true; // 값을 바꿀거에요 -> 조준 해금 상태를 참(true)으로 + Debug.Log("[BossCounter] 조준 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를 } break; - case CounterType.Pierce: - if (!Data.pierceCounterUnlocked) + case CounterType.Pierce: // 조건이 맞으면 실행할거에요 -> 관통 타입이라면 + if (!Data.pierceCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면 { - Data.pierceCounterUnlocked = true; - Debug.Log("[BossCounter] 관통 카운터 영구 잠금 해제!"); + Data.pierceCounterUnlocked = true; // 값을 바꿀거에요 -> 관통 해금 상태를 참(true)으로 + Debug.Log("[BossCounter] 관통 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를 } break; } - Save(); + Save(); // 함수를 실행할거에요 -> 변경된 데이터를 저장하는 Save를 } /// 카운터가 잠금 해제되어 있는지 확인 - public bool IsUnlocked(CounterType type) + public bool IsUnlocked(CounterType type) // 함수를 선언할거에요 -> 해금 여부를 확인하는 IsUnlocked를 { - return type switch + return type switch // 반환할거에요 -> 타입에 따른 해금 변수 값을 { - CounterType.Dodge => Data.dodgeCounterUnlocked, - CounterType.Aim => Data.aimCounterUnlocked, - CounterType.Pierce => Data.pierceCounterUnlocked, - _ => false + CounterType.Dodge => Data.dodgeCounterUnlocked, // 매칭되면 반환할거에요 -> 회피 해금 여부를 + CounterType.Aim => Data.aimCounterUnlocked, // 매칭되면 반환할거에요 -> 조준 해금 여부를 + CounterType.Pierce => Data.pierceCounterUnlocked, // 매칭되면 반환할거에요 -> 관통 해금 여부를 + _ => false // 그 외에는 반환할거에요 -> 거짓(false)을 }; } /// 카운터 발동 횟수 기록 - public void RecordActivation(CounterType type) + public void RecordActivation(CounterType type) // 함수를 선언할거에요 -> 발동 횟수를 기록하는 RecordActivation을 { - switch (type) + switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라 { - case CounterType.Dodge: Data.dodgeCounterActivations++; break; - case CounterType.Aim: Data.aimCounterActivations++; break; - case CounterType.Pierce: Data.pierceCounterActivations++; break; + case CounterType.Dodge: Data.dodgeCounterActivations++; break; // 일치하면 실행할거에요 -> 회피 발동 횟수를 1 증가시키기를 + case CounterType.Aim: Data.aimCounterActivations++; break; // 일치하면 실행할거에요 -> 조준 발동 횟수를 1 증가시키기를 + case CounterType.Pierce: Data.pierceCounterActivations++; break; // 일치하면 실행할거에요 -> 관통 발동 횟수를 1 증가시키기를 } - Save(); + Save(); // 함수를 실행할거에요 -> 변경된 횟수를 저장하는 Save를 } // ═══════════════════════════════════════════ // 저장 / 로드 / 리셋 // ═══════════════════════════════════════════ - public void Save() + public void Save() // 함수를 선언할거에요 -> 데이터를 디스크에 저장하는 Save를 { - string json = JsonUtility.ToJson(Data); - PlayerPrefs.SetString(SAVE_KEY, json); - PlayerPrefs.Save(); + string json = JsonUtility.ToJson(Data); // 변환할거에요 -> 데이터 객체를 JSON 문자열로 + PlayerPrefs.SetString(SAVE_KEY, json); // 저장할거에요 -> JSON 문자열을 PlayerPrefs에 + PlayerPrefs.Save(); // 실행할거에요 -> 변경사항을 디스크에 즉시 쓰기를 } - public void Load() + public void Load() // 함수를 선언할거에요 -> 데이터를 불러오는 Load를 { - if (PlayerPrefs.HasKey(SAVE_KEY)) + if (PlayerPrefs.HasKey(SAVE_KEY)) // 조건이 맞으면 실행할거에요 -> 저장된 키가 존재한다면 { - string json = PlayerPrefs.GetString(SAVE_KEY); - Data = JsonUtility.FromJson(json); + string json = PlayerPrefs.GetString(SAVE_KEY); // 불러올거에요 -> 저장된 JSON 문자열을 + Data = JsonUtility.FromJson(json); // 변환할거에요 -> JSON을 데이터 객체로 복구해서 Data에 } - else + else // 조건이 틀리면 실행할거에요 -> 저장된 데이터가 없다면 { - Data = new BossCounterSaveData(); + Data = new BossCounterSaveData(); // 생성할거에요 -> 새로운 빈 데이터 객체를 } } /// 모든 영구 데이터 초기화 (디버그/뉴게임+) - public void ResetAllData() + public void ResetAllData() // 함수를 선언할거에요 -> 모든 데이터를 초기화하는 ResetAllData를 { - Data = new BossCounterSaveData(); - PlayerPrefs.DeleteKey(SAVE_KEY); - Debug.Log("[BossCounter] 모든 보스 기억 데이터 초기화됨"); + Data = new BossCounterSaveData(); // 초기화할거에요 -> 데이터 객체를 새 것으로 + PlayerPrefs.DeleteKey(SAVE_KEY); // 삭제할거에요 -> 저장된 키를 + Debug.Log("[BossCounter] 모든 보스 기억 데이터 초기화됨"); // 로그를 출력할거에요 -> 초기화 완료 메시지를 } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs b/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs index be1a3ef5..7775a9cb 100644 --- a/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs +++ b/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs @@ -1,213 +1,206 @@ -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.Events; +using System.Collections.Generic; // 리스트와 딕셔너리 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.Events; // 유니티 이벤트 기능을 사용할거에요 -> UnityEngine.Events를 /// /// 보스 카운터 시스템 메인 컨트롤러. /// (수정됨: Config나 Player가 없어도 에러가 나지 않도록 안전장치가 추가된 버전) -/// -/// 핵심 흐름: -/// 1. PlayerBehaviorTracker에서 실시간 행동 데이터를 읽음 -/// 2. 임계치 판단 → 카운터 모드 ON/OFF (잠금 해제 여부에 따라 임계치 다름) -/// 3. 가중치 기반으로 보스 패턴 선택 -/// 4. 첫 발동 시 해당 카운터를 영구 잠금 해제 -/// 5. 플레이어가 습관을 바꾸면 보상 (난이도 완화) /// -public class BossCounterSystem : MonoBehaviour +public class BossCounterSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterSystem을 { - [Header("참조")] - [SerializeField] private BossCounterConfig config; + [Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를 + [SerializeField] private BossCounterConfig config; // 변수를 선언할거에요 -> 카운터 설정 파일(BossCounterConfig)을 담을 config를 - [Header("이벤트 (연출/UI 연동)")] - [Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")] - public UnityEvent OnCounterActivated; + [Header("이벤트 (연출/UI 연동)")] // 인스펙터 창에 제목을 표시할거에요 -> 이벤트 (연출/UI 연동) 을 + [Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public UnityEvent OnCounterActivated; // 이벤트를 선언할거에요 -> 카운터가 켜질 때 알릴 OnCounterActivated를 - [Tooltip("카운터 모드가 비활성화될 때 발생")] - public UnityEvent OnCounterDeactivated; + [Tooltip("카운터 모드가 비활성화될 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public UnityEvent OnCounterDeactivated; // 이벤트를 선언할거에요 -> 카운터가 꺼질 때 알릴 OnCounterDeactivated를 - [Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")] - public UnityEvent OnCounterPatternSelected; + [Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public UnityEvent OnCounterPatternSelected; // 이벤트를 선언할거에요 -> 패턴이 선택됐을 때 알릴 OnCounterPatternSelected를 - [Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")] - public UnityEvent OnHabitChangeRewarded; + [Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public UnityEvent OnHabitChangeRewarded; // 이벤트를 선언할거에요 -> 습관 변경 보상을 알릴 OnHabitChangeRewarded를 // ── 카운터 모드 상태 ── - private Dictionary activeCounters = new Dictionary() + private Dictionary activeCounters = new Dictionary() // 변수를 선언하고 초기화할거에요 -> 각 카운터 타입의 활성 상태를 저장할 딕셔너리 activeCounters를 { - { CounterType.Dodge, false }, - { CounterType.Aim, false }, - { CounterType.Pierce, false } + { CounterType.Dodge, false }, // 값을 넣을거에요 -> 회피 카운터 초기값을 거짓(false)으로 + { CounterType.Aim, false }, // 값을 넣을거에요 -> 조준 카운터 초기값을 거짓(false)으로 + { CounterType.Pierce, false } // 값을 넣을거에요 -> 관통 카운터 초기값을 거짓(false)으로 }; // ── Decay 타이머 ── - private Dictionary counterTimers = new Dictionary() + private Dictionary counterTimers = new Dictionary() // 변수를 선언하고 초기화할거에요 -> 각 카운터의 남은 지속 시간을 저장할 딕셔너리 counterTimers를 { - { CounterType.Dodge, 0f }, - { CounterType.Aim, 0f }, - { CounterType.Pierce, 0f } + { CounterType.Dodge, 0f }, // 값을 넣을거에요 -> 회피 카운터 타이머를 0으로 + { CounterType.Aim, 0f }, // 값을 넣을거에요 -> 조준 카운터 타이머를 0으로 + { CounterType.Pierce, 0f } // 값을 넣을거에요 -> 관통 카운터 타이머를 0으로 }; // ── 쿨타임 ── - private float lastCounterPatternTime = -100f; + private float lastCounterPatternTime = -100f; // 변수를 선언할거에요 -> 마지막 카운터 패턴 사용 시간을 lastCounterPatternTime에 (초기값 -100) // ── 습관 변경 보상 추적 ── - private HashSet previousRunCounters = new HashSet(); - private HashSet currentRunActivatedCounters = new HashSet(); - private bool habitChangeRewardGranted = false; + private HashSet previousRunCounters = new HashSet(); // 변수를 선언할거에요 -> 지난 런에서 당했던 카운터 목록을 previousRunCounters에 + private HashSet currentRunActivatedCounters = new HashSet(); // 변수를 선언할거에요 -> 이번 런에서 발동된 카운터 목록을 currentRunActivatedCounters에 + private bool habitChangeRewardGranted = false; // 변수를 선언할거에요 -> 보상이 이미 지급되었는지 여부를 habitChangeRewardGranted에 // ── 보스 패턴 가중치 정의 ── - [System.Serializable] - public class BossPattern + [System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정할 수 있게 BossPattern 클래스를 + public class BossPattern // 내부 클래스를 선언할거에요 -> 보스 패턴 정보를 담을 BossPattern을 { - public string patternName; - public float baseWeight = 1f; - [Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")] - public CounterType counterType = CounterType.None; - [Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")] - public CounterType subCounterType = CounterType.None; + public string patternName; // 변수를 선언할거에요 -> 패턴 이름을 저장할 patternName을 + public float baseWeight = 1f; // 변수를 선언할거에요 -> 기본 가중치(확률)를 baseWeight에 + [Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public CounterType counterType = CounterType.None; // 변수를 선언할거에요 -> 이 패턴이 어떤 카운터 타입인지 지정할 counterType을 + [Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + public CounterType subCounterType = CounterType.None; // 변수를 선언할거에요 -> 보너스 가중치를 받을 보조 카운터 타입 subCounterType을 } - [Header("보스 패턴 목록")] - [SerializeField] private List bossPatterns = new List(); + [Header("보스 패턴 목록")] // 인스펙터 창에 제목을 표시할거에요 -> 보스 패턴 목록 을 + [SerializeField] private List bossPatterns = new List(); // 리스트를 선언할거에요 -> 보스의 모든 패턴 정보를 담을 bossPatterns를 // ═══════════════════════════════════════════ // 초기화 // ═══════════════════════════════════════════ - private void Start() + private void Start() // 함수를 실행할거에요 -> 게임 시작 시 호출되는 Start를 { // 🚨 [안전장치] 설정 파일이 없으면 경고 출력 - if (config == null) + if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일(config)이 연결되지 않았다면 { - Debug.LogWarning("⚠ [BossCounterSystem] Config(설정 파일)가 없습니다! AI가 정상 작동하지 않을 수 있습니다."); + Debug.LogWarning("⚠ [BossCounterSystem] Config(설정 파일)가 없습니다! AI가 정상 작동하지 않을 수 있습니다."); // 경고 로그를 출력할거에요 -> 설정 파일 누락 경고를 } } /// /// 보스 전투 시작 시 호출. 이전 런에서 발동된 카운터 정보를 기반으로 초기화. /// - public void InitializeBattle() + public void InitializeBattle() // 함수를 선언할거에요 -> 전투 시작 시 초기화를 담당할 InitializeBattle을 { // 이전 런에서 잠금 해제된 카운터들을 기록 (습관 변경 보상 판단용) - previousRunCounters.Clear(); - currentRunActivatedCounters.Clear(); - habitChangeRewardGranted = false; + previousRunCounters.Clear(); // 비울거에요 -> 이전 런 카운터 목록을 + currentRunActivatedCounters.Clear(); // 비울거에요 -> 이번 런 카운터 목록을 + habitChangeRewardGranted = false; // 초기화할거에요 -> 보상 지급 여부를 거짓(false)으로 - var persistence = BossCounterPersistence.Instance; - if (persistence != null) + var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에 + if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 존재한다면 { - if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge)) - previousRunCounters.Add(CounterType.Dodge); - if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim)) - previousRunCounters.Add(CounterType.Aim); - if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce)) - previousRunCounters.Add(CounterType.Pierce); + if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge)) // 조건이 맞으면 실행할거에요 -> 회피 카운터 기록이 있고 해제되었다면 + previousRunCounters.Add(CounterType.Dodge); // 추가할거에요 -> 회피 타입을 이전 런 목록에 + if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim)) // 조건이 맞으면 실행할거에요 -> 조준 카운터 기록이 있고 해제되었다면 + previousRunCounters.Add(CounterType.Aim); // 추가할거에요 -> 조준 타입을 이전 런 목록에 + if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce)) // 조건이 맞으면 실행할거에요 -> 관통 카운터 기록이 있고 해제되었다면 + previousRunCounters.Add(CounterType.Pierce); // 추가할거에요 -> 관통 타입을 이전 런 목록에 } // 모든 카운터 모드 OFF - foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) + foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) // 반복할거에요 -> 모든 카운터 타입(회피, 조준, 관통)에 대해 { - activeCounters[type] = false; - counterTimers[type] = 0f; + activeCounters[type] = false; // 상태를 바꿀거에요 -> 해당 카운터를 비활성화(false)로 + counterTimers[type] = 0f; // 값을 바꿀거에요 -> 해당 카운터 타이머를 0으로 } - lastCounterPatternTime = -100f; + lastCounterPatternTime = -100f; // 값을 초기화할거에요 -> 마지막 패턴 사용 시간을 -100으로 - Debug.Log($"[BossCounter] 전투 시작. 이전 런 카운터: [{string.Join(", ", previousRunCounters)}]"); + Debug.Log($"[BossCounter] 전투 시작. 이전 런 카운터: [{string.Join(", ", previousRunCounters)}]"); // 로그를 출력할거에요 -> 전투 시작 알림과 이전 런 정보를 } // ═══════════════════════════════════════════ // 매 프레임 업데이트 // ═══════════════════════════════════════════ - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 { // 🚨 [안전장치] 플레이어 트래커나 설정 파일이 없으면 실행 중지 (NullReferenceException 방지) - if (PlayerBehaviorTracker.Instance == null || config == null) return; + if (PlayerBehaviorTracker.Instance == null || config == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 추적기나 설정 파일이 없다면 - EvaluateCounters(); - DecayCounters(); - CheckHabitChangeReward(); + EvaluateCounters(); // 함수를 실행할거에요 -> 카운터 발동 조건을 검사하는 EvaluateCounters를 + DecayCounters(); // 함수를 실행할거에요 -> 카운터 지속 시간을 관리하는 DecayCounters를 + CheckHabitChangeReward(); // 함수를 실행할거에요 -> 습관 변경 보상을 체크하는 CheckHabitChangeReward를 } /// 플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단 - private void EvaluateCounters() + private void EvaluateCounters() // 함수를 선언할거에요 -> 카운터 발동 여부를 판단하는 EvaluateCounters를 { // 🚨 [안전장치] Config가 없으면 계산 불가 - if (config == null) return; + if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면 - var tracker = PlayerBehaviorTracker.Instance; - var persistence = BossCounterPersistence.Instance; + var tracker = PlayerBehaviorTracker.Instance; // 변수를 가져올거에요 -> 플레이어 행동 추적기를 tracker에 + var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에 // ── 회피 카운터 ── - bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge); - int dodgeThreshold = config.GetEffectiveDodgeThreshold(dodgeUnlocked); + bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge); // 값을 확인할거에요 -> 회피 카운터가 해금되었는지 여부를 + int dodgeThreshold = config.GetEffectiveDodgeThreshold(dodgeUnlocked); // 값을 가져올거에요 -> 현재 적용할 회피 임계값을 - if (tracker.DodgeCount >= dodgeThreshold) + if (tracker.DodgeCount >= dodgeThreshold) // 조건이 맞으면 실행할거에요 -> 플레이어 회피 횟수가 임계값 이상이라면 { // 잠금 해제 안 된 상태면 첫 발동 시 잠금 해제 (첫 런에서도 발동 가능) // 잠금 해제된 상태면 더 낮은 임계치로 발동 - ActivateCounter(CounterType.Dodge); + ActivateCounter(CounterType.Dodge); // 함수를 실행할거에요 -> 회피 카운터를 발동시키는 ActivateCounter를 } // ── 조준 카운터 ── - bool aimUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Aim); - float aimThreshold = config.GetEffectiveAimThreshold(aimUnlocked); + bool aimUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Aim); // 값을 확인할거에요 -> 조준 카운터가 해금되었는지 여부를 + float aimThreshold = config.GetEffectiveAimThreshold(aimUnlocked); // 값을 가져올거에요 -> 현재 적용할 조준 임계값을 - if (tracker.AimHoldTime >= aimThreshold) + if (tracker.AimHoldTime >= aimThreshold) // 조건이 맞으면 실행할거에요 -> 플레이어 조준 시간이 임계값 이상이라면 { - ActivateCounter(CounterType.Aim); + ActivateCounter(CounterType.Aim); // 함수를 실행할거에요 -> 조준 카운터를 발동시키는 ActivateCounter를 } // ── 관통 카운터 ── - bool pierceUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Pierce); - float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked); + bool pierceUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Pierce); // 값을 확인할거에요 -> 관통 카운터가 해금되었는지 여부를 + float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked); // 값을 가져올거에요 -> 현재 적용할 관통 임계값을 - if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck - && tracker.PierceRatio >= pierceThreshold) + if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck // 조건이 맞으면 실행할거에요 -> 최소 발사 횟수를 충족하고 + && tracker.PierceRatio >= pierceThreshold) // 조건이 맞으면 실행할거에요 -> 관통 공격 비율이 임계값 이상이라면 { - ActivateCounter(CounterType.Pierce); + ActivateCounter(CounterType.Pierce); // 함수를 실행할거에요 -> 관통 카운터를 발동시키는 ActivateCounter를 } } - private void ActivateCounter(CounterType type) + private void ActivateCounter(CounterType type) // 함수를 선언할거에요 -> 특정 카운터를 활성화하는 ActivateCounter를 { // 🚨 [안전장치] Config 확인 - if (config == null) return; + if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면 - counterTimers[type] = config.counterDecayTime; + counterTimers[type] = config.counterDecayTime; // 값을 설정할거에요 -> 해당 카운터의 지속 시간을 설정값으로 - if (!activeCounters[type]) + if (!activeCounters[type]) // 조건이 맞으면 실행할거에요 -> 해당 카운터가 현재 꺼져 있다면 { - activeCounters[type] = true; - currentRunActivatedCounters.Add(type); + activeCounters[type] = true; // 상태를 바꿀거에요 -> 카운터 활성 상태를 참(true)으로 + currentRunActivatedCounters.Add(type); // 추가할거에요 -> 이번 런 발동 목록에 해당 타입을 // 영구 잠금 해제 - var persistence = BossCounterPersistence.Instance; - if (persistence != null) + var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에 + if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면 { - persistence.UnlockCounter(type); - persistence.RecordActivation(type); + persistence.UnlockCounter(type); // 함수를 실행할거에요 -> 해당 카운터를 영구 해금하는 UnlockCounter를 + persistence.RecordActivation(type); // 함수를 실행할거에요 -> 발동 횟수를 기록하는 RecordActivation을 } - OnCounterActivated?.Invoke(type); - Debug.Log($"[BossCounter] {type} 카운터 활성화!"); + OnCounterActivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 활성화 알림 이벤트를 + Debug.Log($"[BossCounter] {type} 카운터 활성화!"); // 로그를 출력할거에요 -> 카운터 활성화 메시지를 } } /// 시간이 지나면 카운터 모드 자동 해제 - private void DecayCounters() + private void DecayCounters() // 함수를 선언할거에요 -> 시간이 지나면 카운터를 끄는 DecayCounters를 { - foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) + foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) // 반복할거에요 -> 모든 카운터 타입에 대해 { - if (!activeCounters[type]) continue; + if (!activeCounters[type]) continue; // 조건이 맞으면 건너뛸거에요 -> 이미 꺼져 있는 카운터라면 - counterTimers[type] -= Time.deltaTime; - if (counterTimers[type] <= 0f) + counterTimers[type] -= Time.deltaTime; // 값을 뺄거에요 -> 남은 시간에서 프레임 시간을 + if (counterTimers[type] <= 0f) // 조건이 맞으면 실행할거에요 -> 남은 시간이 0 이하라면 { - activeCounters[type] = false; - OnCounterDeactivated?.Invoke(type); - Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화"); + activeCounters[type] = false; // 상태를 바꿀거에요 -> 카운터 활성 상태를 거짓(false)으로 + OnCounterDeactivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 비활성화 알림 이벤트를 + Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화"); // 로그를 출력할거에요 -> 시간 만료로 인한 비활성화 메시지를 } } } @@ -220,25 +213,25 @@ public class BossCounterSystem : MonoBehaviour /// 이전 런에서 카운터 당한 습관을 이번 런에서 보이지 않으면 보상. /// 전투 중반(30초 이후) 한 번 체크. /// - private float battleTimer = 0f; - private const float HABIT_CHECK_TIME = 30f; + private float battleTimer = 0f; // 변수를 초기화할거에요 -> 전투 진행 시간을 0으로 + private const float HABIT_CHECK_TIME = 30f; // 상수를 정의할거에요 -> 습관 체크 시점을 30초로 - private void CheckHabitChangeReward() + private void CheckHabitChangeReward() // 함수를 선언할거에요 -> 습관 변경 보상을 확인하는 CheckHabitChangeReward를 { - if (habitChangeRewardGranted || previousRunCounters.Count == 0) return; + if (habitChangeRewardGranted || previousRunCounters.Count == 0) return; // 조건이 맞으면 중단할거에요 -> 이미 보상을 받았거나 이전 런 기록이 없다면 - battleTimer += Time.deltaTime; - if (battleTimer < HABIT_CHECK_TIME) return; + battleTimer += Time.deltaTime; // 값을 더할거에요 -> 전투 진행 시간에 프레임 시간을 + if (battleTimer < HABIT_CHECK_TIME) return; // 조건이 맞으면 중단할거에요 -> 아직 30초가 안 지났다면 // 이전 런에서 발동된 카운터 중, 이번 런에서 아직 발동 안 된 것이 있으면 보상 - foreach (var prevCounter in previousRunCounters) + foreach (var prevCounter in previousRunCounters) // 반복할거에요 -> 이전 런 카운터 목록에 대해 { - if (!currentRunActivatedCounters.Contains(prevCounter)) + if (!currentRunActivatedCounters.Contains(prevCounter)) // 조건이 맞으면 실행할거에요 -> 이번 런 목록에 해당 카운터가 없다면 (습관 고침) { - habitChangeRewardGranted = true; - OnHabitChangeRewarded?.Invoke(); - Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여"); - break; + habitChangeRewardGranted = true; // 상태를 바꿀거에요 -> 보상 지급 여부를 참(true)으로 + OnHabitChangeRewarded?.Invoke(); // 이벤트를 실행할거에요 -> 보상 지급 알림 이벤트를 + Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여"); // 로그를 출력할거에요 -> 보상 부여 메시지를 + break; // 중단할거에요 -> 보상은 한 번만 주므로 반복문을 } } } @@ -252,106 +245,106 @@ public class BossCounterSystem : MonoBehaviour /// 보스 AI의 패턴 선택 시점에 호출하세요. /// /// 선택된 패턴 이름 - public string SelectBossPattern() + public string SelectBossPattern() // 함수를 선언할거에요 -> 보스 패턴을 선택해서 반환하는 SelectBossPattern을 { - if (bossPatterns.Count == 0) + if (bossPatterns.Count == 0) // 조건이 맞으면 실행할거에요 -> 등록된 패턴이 하나도 없다면 { - Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!"); - return "Normal"; // 기본값 리턴 + Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!"); // 경고 로그를 출력할거에요 -> 패턴 없음 경고를 + return "Normal"; // 값을 반환할거에요 -> 기본값 "Normal"을 } // 🚨 [안전장치] Config가 없으면 그냥 랜덤 패턴 반환 (멈춤 방지) - if (config == null) + if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일이 없다면 { - int randomIndex = Random.Range(0, bossPatterns.Count); - return bossPatterns[randomIndex].patternName; + int randomIndex = Random.Range(0, bossPatterns.Count); // 값을 랜덤으로 뽑을거에요 -> 0부터 패턴 개수 사이의 인덱스를 + return bossPatterns[randomIndex].patternName; // 값을 반환할거에요 -> 랜덤하게 뽑힌 패턴 이름을 } // 가중치 계산 - float[] weights = new float[bossPatterns.Count]; - float totalWeight = 0f; - float counterWeight = 0f; + float[] weights = new float[bossPatterns.Count]; // 배열을 만들거에요 -> 각 패턴의 가중치를 담을 weights 배열을 + float totalWeight = 0f; // 변수를 초기화할거에요 -> 전체 가중치 합계를 0으로 + float counterWeight = 0f; // 변수를 초기화할거에요 -> 카운터 패턴들의 가중치 합계를 0으로 - for (int i = 0; i < bossPatterns.Count; i++) + for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해 { - weights[i] = bossPatterns[i].baseWeight; + weights[i] = bossPatterns[i].baseWeight; // 값을 넣을거에요 -> 기본 가중치를 배열에 // 카운터 패턴 가중치 증가 - if (bossPatterns[i].counterType != CounterType.None - && activeCounters.ContainsKey(bossPatterns[i].counterType) - && activeCounters[bossPatterns[i].counterType]) + if (bossPatterns[i].counterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 카운터 타입이 설정되어 있고 + && activeCounters.ContainsKey(bossPatterns[i].counterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며 + && activeCounters[bossPatterns[i].counterType]) // 조건이 맞으면 실행할거에요 -> 해당 카운터가 활성 상태라면 { // 쿨타임 체크 - if (Time.time - lastCounterPatternTime >= config.counterCooldown) + if (Time.time - lastCounterPatternTime >= config.counterCooldown) // 조건이 맞으면 실행할거에요 -> 마지막 사용 후 쿨타임이 지났다면 { - weights[i] += config.counterWeightBonus; + weights[i] += config.counterWeightBonus; // 값을 더할거에요 -> 카운터 보너스 가중치를 } } // 보조 카운터 가중치 - if (bossPatterns[i].subCounterType != CounterType.None - && activeCounters.ContainsKey(bossPatterns[i].subCounterType) - && activeCounters[bossPatterns[i].subCounterType]) + if (bossPatterns[i].subCounterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 보조 카운터 타입이 설정되어 있고 + && activeCounters.ContainsKey(bossPatterns[i].subCounterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며 + && activeCounters[bossPatterns[i].subCounterType]) // 조건이 맞으면 실행할거에요 -> 해당 보조 카운터가 활성 상태라면 { - weights[i] += config.counterSubWeightBonus; + weights[i] += config.counterSubWeightBonus; // 값을 더할거에요 -> 보조 카운터 보너스 가중치를 } // 습관 변경 보상: 일반 패턴 가중치 감소 (= 난이도 완화) - if (habitChangeRewardGranted && bossPatterns[i].counterType == CounterType.None) + if (habitChangeRewardGranted && bossPatterns[i].counterType == CounterType.None) // 조건이 맞으면 실행할거에요 -> 보상을 받았고 일반 패턴이라면 { - weights[i] *= (1f - config.habitChangeRewardRatio); + weights[i] *= (1f - config.habitChangeRewardRatio); // 값을 곱할거에요 -> 가중치를 감소 비율만큼 줄여서 } - totalWeight += weights[i]; + totalWeight += weights[i]; // 값을 더할거에요 -> 전체 가중치 합계에 현재 가중치를 - if (bossPatterns[i].counterType != CounterType.None) - counterWeight += weights[i]; + if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면 + counterWeight += weights[i]; // 값을 더할거에요 -> 카운터 가중치 합계에 } // 빈도 상한 체크: 카운터 패턴 총 비율이 maxCounterFrequency를 넘지 않도록 - if (totalWeight > 0f && counterWeight / totalWeight > config.maxCounterFrequency) + if (totalWeight > 0f && counterWeight / totalWeight > config.maxCounterFrequency) // 조건이 맞으면 실행할거에요 -> 카운터 패턴 비율이 최대 허용치를 초과한다면 { - float allowedCounterWeight = (totalWeight - counterWeight) * config.maxCounterFrequency / (1f - config.maxCounterFrequency); - float scale = allowedCounterWeight / counterWeight; + float allowedCounterWeight = (totalWeight - counterWeight) * config.maxCounterFrequency / (1f - config.maxCounterFrequency); // 값을 계산할거에요 -> 허용 가능한 카운터 총 가중치를 + float scale = allowedCounterWeight / counterWeight; // 값을 계산할거에요 -> 가중치를 줄일 비율(scale)을 - for (int i = 0; i < bossPatterns.Count; i++) + for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해 { - if (bossPatterns[i].counterType != CounterType.None) + if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면 { - float bonus = weights[i] - bossPatterns[i].baseWeight; - weights[i] = bossPatterns[i].baseWeight + bonus * scale; + float bonus = weights[i] - bossPatterns[i].baseWeight; // 값을 계산할거에요 -> 추가된 보너스 가중치를 + weights[i] = bossPatterns[i].baseWeight + bonus * scale; // 값을 수정할거에요 -> 보너스를 비율대로 줄여서 다시 적용 } } // 총 가중치 재계산 - totalWeight = 0f; - for (int i = 0; i < weights.Length; i++) - totalWeight += weights[i]; + totalWeight = 0f; // 값을 초기화할거에요 -> 전체 합계를 0으로 + for (int i = 0; i < weights.Length; i++) // 반복할거에요 -> 모든 가중치에 대해 + totalWeight += weights[i]; // 값을 더할거에요 -> 전체 합계에 } // 가중치 랜덤 선택 - float roll = Random.Range(0f, totalWeight); - float cumulative = 0f; + float roll = Random.Range(0f, totalWeight); // 값을 랜덤으로 뽑을거에요 -> 0부터 전체 가중치 사이의 값을 roll에 + float cumulative = 0f; // 변수를 초기화할거에요 -> 누적 가중치를 0으로 - for (int i = 0; i < bossPatterns.Count; i++) + for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해 { - cumulative += weights[i]; - if (roll <= cumulative) + cumulative += weights[i]; // 값을 더할거에요 -> 누적 가중치에 현재 가중치를 + if (roll <= cumulative) // 조건이 맞으면 실행할거에요 -> 랜덤값이 누적치보다 작거나 같다면 (당첨) { - string selected = bossPatterns[i].patternName; + string selected = bossPatterns[i].patternName; // 값을 저장할거에요 -> 선택된 패턴 이름을 selected에 // 카운터 패턴이면 쿨타임 기록 - if (bossPatterns[i].counterType != CounterType.None) + if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면 { - lastCounterPatternTime = Time.time; + lastCounterPatternTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 사용 시간으로 } - OnCounterPatternSelected?.Invoke(selected); - return selected; + OnCounterPatternSelected?.Invoke(selected); // 이벤트를 실행할거에요 -> 패턴 선택 알림 이벤트를 + return selected; // 값을 반환할거에요 -> 선택된 패턴 이름을 } } - return bossPatterns[bossPatterns.Count - 1].patternName; + return bossPatterns[bossPatterns.Count - 1].patternName; // 값을 반환할거에요 -> 만약 선택되지 않았다면 마지막 패턴을 (안전장치) } // ═══════════════════════════════════════════ @@ -359,22 +352,22 @@ public class BossCounterSystem : MonoBehaviour // ═══════════════════════════════════════════ /// 특정 카운터 모드가 현재 활성 상태인지 확인 - public bool IsCounterActive(CounterType type) + public bool IsCounterActive(CounterType type) // 함수를 선언할거에요 -> 특정 카운터 활성 여부를 반환하는 IsCounterActive를 { - return activeCounters.ContainsKey(type) && activeCounters[type]; + return activeCounters.ContainsKey(type) && activeCounters[type]; // 값을 반환할거에요 -> 딕셔너리에 키가 있고 값이 참(true)인지를 } /// 현재 활성화된 모든 카운터 타입 반환 - public List GetActiveCounters() + public List GetActiveCounters() // 함수를 선언할거에요 -> 활성화된 모든 카운터 목록을 반환하는 GetActiveCounters를 { - var result = new List(); - foreach (var kvp in activeCounters) + var result = new List(); // 리스트를 만들거에요 -> 결과를 담을 result 리스트를 + foreach (var kvp in activeCounters) // 반복할거에요 -> 모든 카운터 상태에 대해 { - if (kvp.Value) result.Add(kvp.Key); + if (kvp.Value) result.Add(kvp.Key); // 조건이 맞으면 실행할거에요 -> 활성화(true) 상태라면 리스트에 추가하기를 } - return result; + return result; // 값을 반환할거에요 -> 완성된 리스트를 } /// 습관 변경 보상이 활성화되었는지 확인 - public bool IsHabitChangeRewarded => habitChangeRewardGranted; + public bool IsHabitChangeRewarded => habitChangeRewardGranted; // 프로퍼티를 선언할거에요 -> 보상 지급 여부를 외부에서 읽을 수 있는 IsHabitChangeRewarded를 } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/BossMonster.cs b/Assets/Scripts/Enemy/BossAI/BossMonster.cs index b92364de..37db27b7 100644 --- a/Assets/Scripts/Enemy/BossAI/BossMonster.cs +++ b/Assets/Scripts/Enemy/BossAI/BossMonster.cs @@ -1,460 +1,443 @@ -using UnityEngine; -using System.Collections; +using UnityEngine; // 유니티 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 -public class NorcielBoss : MonsterClass +public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를 { - [Header("--- 🧠 두뇌 연결 ---")] - [SerializeField] private BossCounterSystem counterSystem; + [Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🧠 두뇌 연결 ---을 + [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 보스의 패턴을 결정할 counterSystem을 - [Header("--- ⚔️ 패턴 설정 ---")] - [SerializeField] private float patternInterval = 3f; + [Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- ⚔️ 패턴 설정 ---을 + [SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 대기 시간을 3초로 지정하는 patternInterval을 + [SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 이 거리 안에 플레이어가 들어오면 공격하는 attackRange를 3으로 - [Header("--- 🎱 무기(쇠공) 설정 (필수!) ---")] - [SerializeField] private GameObject ironBall; - [SerializeField] private Transform handHolder; + [Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🎱 무기 설정 ---을 + [SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 던지고 주울 무기 오브젝트인 ironBall을 + [SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 무기를 잡을 손의 위치인 handHolder를 - [Header("--- 📊 UI 연결 ---")] - [SerializeField] private GameObject bossHealthBar; + [Header("--- 📊 UI 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 📊 UI 연결 ---을 + [SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 보스의 체력 UI를 담을 bossHealthBar를 + + // ⭐ [핵심 수정] 애니메이션 이름을 인스펙터에서 맞출 수 있게 변수로 분리 + [Header("--- 🎬 애니메이션 이름 설정 (Animator와 일치시킬 것!) ---")] + [SerializeField] private string anim_Roar = "Roar"; // 변수를 선언할거에요 -> 포효 애니메이션 이름을 + [SerializeField] private string anim_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름을 + [SerializeField] private string anim_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 + [SerializeField] private string anim_Pickup = "Skill_Pickup"; // 변수를 선언할거에요 -> 무기 줍기 애니메이션 이름을 + [SerializeField] private string anim_Throw = "Attack_Throw"; // 변수를 선언할거에요 -> 던지기 애니메이션 이름을 + + [Header("--- 🎬 스킬 애니메이션 이름 ---")] + [SerializeField] private string anim_DashReady = "Skill_Dash_Ready"; // 변수를 선언할거에요 -> 돌진 준비 애니메이션 이름을 + [SerializeField] private string anim_DashGo = "Skill_Dash_Go"; // 변수를 선언할거에요 -> 돌진 출발 애니메이션 이름을 + [SerializeField] private string anim_SmashCharge = "Skill_Smash_Charge"; // 변수를 선언할거에요 -> 내려찍기 기모으기 이름을 + [SerializeField] private string anim_SmashImpact = "Skill_Smash_Impact"; // 변수를 선언할거에요 -> 내려찍기 타격 이름을 + [SerializeField] private string anim_Sweep = "Skill_Sweep"; // 변수를 선언할거에요 -> 휩쓸기 애니메이션 이름을 + + [Header("--- 🔍 디버그 (읽기 전용) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🔍 디버그 ---를 + [SerializeField] private string debugState = "대기 중"; // 변수를 선언할거에요 -> 현재 보스 행동을 문장으로 보여줄 debugState를 // --- 내부 변수들 --- - private float _timer; - private Rigidbody rb; - private Rigidbody ballRb; + private float _timer; // 변수를 선언할거에요 -> 공격 쿨타임을 계산할 내부 타이머 _timer를 + private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 엔진을 제어할 rb를 + private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 엔진을 제어할 ballRb를 - private bool isBattleStarted = false; - private bool isWeaponless = false; - private bool isPerformingAction = false; // ⭐ 신규: 연출 중 플래그 (Roar 등) - private Transform target; + private bool isBattleStarted = false; // 변수를 초기화할거에요 -> 전투 시작 상태를 거짓(false)으로 + private bool isWeaponless = false; // 변수를 초기화할거에요 -> 맨손 상태를 거짓(false)으로 + private bool isPerformingAction = false; // 변수를 초기화할거에요 -> 연출 진행 상태를 거짓(false)으로 + private Transform target; // 변수를 선언할거에요 -> 보스가 쫓아갈 대상의 위치인 target을 - protected override void Awake() + protected override void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를 { - base.Awake(); - rb = GetComponent(); - if (ironBall != null) ballRb = ironBall.GetComponent(); + base.Awake(); // 부모 클래스의 함수를 먼저 실행할거에요 -> MonsterClass의 Awake 로직을 + rb = GetComponent(); // 컴포넌트를 가져와서 저장할거에요 -> 보스 자신의 Rigidbody를 rb에 + if (ironBall != null) ballRb = ironBall.GetComponent(); // 조건이 맞으면 가져올거에요 -> ironBall이 있다면 그것의 Rigidbody를 ballRb에 } - // ⭐ [수정] Start → OnEnable 이후 호출되도록 변경 - private void Start() + private void Start() // 함수를 실행할거에요 -> 첫 프레임이 시작될 때 호출되는 Start를 { - StartBossBattle(); + StartBossBattle(); // 함수를 실행할거에요 -> Roar 연출 후 전투를 시작하는 StartBossBattle을 } - protected override void Init() + protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 몬스터 초기화를 담당하는 Init을 { - _timer = patternInterval; - isBattleStarted = false; - isWeaponless = false; - isPerformingAction = false; + base.Init(); // 부모 초기화 호출 + _timer = 0f; // 값을 넣을거에요 -> 전투 시작하자마자 첫 공격이 가능하게 타이머를 0으로 + isBattleStarted = false; // 상태를 바꿀거에요 -> 전투 시작 상태를 거짓(false)으로 + isWeaponless = false; // 상태를 바꿀거에요 -> 맨손 상태를 거짓(false)으로 + isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 상태를 거짓(false)으로 - // 플레이어 찾기 - GameObject playerObj = GameObject.FindWithTag("Player"); - if (playerObj != null) target = playerObj.transform; - else + IsAggroed = true; // 상태를 바꿀거에요 -> 부모 클래스의 영구 어그로 상태를 참(true)으로 + mobRenderer = null; // 값을 지울거에요 -> 화면 밖 최적화를 끄기 위해 렌더러를 null로 + optimizationDistance = 9999f; // 값을 바꿀거에요 -> 거리가 멀어져도 멈추지 않게 최적화 거리를 9999로 + + GameObject playerObj = GameObject.FindWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그가 붙은 녀석을 + if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 태그로 찾은 플레이어가 존재한다면 { - var playerScript = FindObjectOfType(); - if (playerScript != null) target = playerScript.transform; + target = playerObj.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에 + playerTransform = playerObj.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를 + } + else // 조건이 틀리면 실행할거에요 -> 태그로 찾지 못했다면 + { + var playerScript = FindObjectOfType(); // 컴포넌트를 찾을거에요 -> PlayerMovement 스크립트를 가진 오브젝트를 + if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 스크립트로 플레이어를 찾았다면 + { + target = playerScript.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에 + playerTransform = playerScript.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를 + } } - // HP 초기화 - currentHP = maxHP; + if (target == null) // 조건이 맞으면 실행할거에요 -> 어떤 방법으로도 플레이어를 못 찾았다면 + Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 출력할거에요 -> 플레이어 없음 경고 메시지를 - StartCoroutine(SafeInitNavMesh()); + currentHP = maxHP; // 체력을 채울거에요 -> 현재 체력을 최대 체력으로 + StartCoroutine(SafeInitNavMesh()); // 코루틴을 실행할거에요 -> 길찾기를 안전하게 켜는 SafeInitNavMesh를 - if (bossHealthBar != null) bossHealthBar.SetActive(false); - if (ballRb != null) ballRb.isKinematic = true; + if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를 + if (ballRb != null) ballRb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 쇠공 물리가 있다면 중력 연산을 끄기를 + if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 대기 모션을 } - private IEnumerator SafeInitNavMesh() + private IEnumerator SafeInitNavMesh() // 코루틴 함수를 선언할거에요 -> 길찾기를 안전하게 초기화하는 SafeInitNavMesh를 { - yield return null; - - if (agent != null) + yield return null; // 기다릴거에요 -> 다음 프레임이 될 때까지 한 턴을 + if (agent != null) // 조건이 맞으면 실행할거에요 -> NavMeshAgent가 존재한다면 { - int retry = 0; - while (!agent.isOnNavMesh && retry < 5) + int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로 + while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 바닥에 없고 시도가 5번 미만인 동안 { - retry++; - yield return new WaitForSeconds(0.1f); + retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼 + yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을 } + if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 바닥에 닿았다면 이동을 정지로 + agent.enabled = false; // 기능을 끌거에요 -> 전투 시작 전까지 NavMeshAgent를 비활성화로 + } + } - if (agent.isOnNavMesh) - { - agent.isStopped = true; - } - agent.enabled = false; + protected override void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 + { + base.Update(); // 부모 클래스의 Update도 호출 (최적화 로직 등) + + if (!isDead) // 조건이 맞으면 실행할거에요 -> 보스가 살아있다면 + { + ExecuteAILogic(); // 함수를 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을 } } // ════════════════════════════════════════ - // ⭐ [핵심 수정] 부모의 OnManagedUpdate 흐름 제어 + // 🧠 AI 핵심 로직 // ════════════════════════════════════════ - /// - /// 부모 MonsterClass의 StopMovement()가 매 프레임 Monster_Idle을 - /// 강제 재생해서 Roar/다른 애니메이션을 덮어쓰는 문제 해결. - /// - /// 보스는 일반 몬스터와 다르게: - /// - 전투 시작 전에는 아무 AI도 돌리지 않음 - /// - 연출 중(Roar 등)에는 애니메이션을 건드리지 않음 - /// - protected override void ExecuteAILogic() + protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을 { - // 연출 중이거나 전투 미시작 → 아무것도 안 함 - if (isPerformingAction || !isBattleStarted || target == null) return; + if (isPerformingAction || !isBattleStarted || target == null) return; // 조건이 맞으면 중단할거에요 -> 연출중, 전투전, 타겟없음 중 하나라도 해당되면 - // 무기 없으면 회수 우선 - if (isWeaponless) + if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 쇠공을 던져서 맨손이라면 { - RetrieveWeaponLogic(); - return; + RetrieveWeaponLogic(); // 함수를 실행할거에요 -> 쇠공을 주우러 가는 로직을 + return; // 중단할거에요 -> 무기 줍기가 우선이므로 아래 로직을 } - // --- 일반 전투 로직 --- - _timer -= Time.deltaTime; - if (_timer <= 0 && !isAttacking && !isHit && !isDead && !isResting) + if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 AI 판단을 + + if (_timer > 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 남아있다면 { - _timer = patternInterval; - DecideAttack(); + _timer -= Time.deltaTime; // 값을 뺄거에요 -> 쿨타임 타이머에서 지난 프레임 시간만큼을 } - // 이동 (공격/피격 중이 아닐 때만) - if (!isAttacking && !isHit && !isResting && agent != null && agent.enabled && agent.isOnNavMesh) + float distToPlayer = Vector3.Distance(transform.position, target.position); // 거리를 계산할거에요 -> 보스와 플레이어 사이의 거리를 + + if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 길찾기가 불가능한 상태라면 + + if (distToPlayer > attackRange) // 조건이 맞으면 실행할거에요 -> 플레이어가 공격 사거리 밖이라면 { - agent.SetDestination(target.position); + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고 + agent.SetDestination(target.position); // 명령을 내릴거에요 -> 길찾기 목표를 플레이어 위치로 + LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을 + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걷기 애니메이션을 맞추는 UpdateMovementAnimation을 + } + else // 조건이 틀리면 실행할거에요 -> 플레이어가 사거리 안에 있다면 + { + agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 + agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 완전한 0으로 + LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을 - if (animator != null) animator.SetFloat("Speed", agent.velocity.magnitude); - - if (agent.remainingDistance <= agent.stoppingDistance + 0.5f) + if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 존재한다면 { - agent.isStopped = true; - LookAtTarget(target.position); - if (animator != null) animator.SetFloat("Speed", 0f); + AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를 + if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷기 모션이 재생 중이라면 + { + animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로 + } } - else + + if (_timer <= 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 0 이하라면 { - agent.isStopped = false; + _timer = patternInterval; // 값을 넣을거에요 -> 쿨타임을 다시 패턴 대기 시간으로 + Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 출력할거에요 -> 공격 시점과 거리 정보를 + DecideAttack(); // 함수를 실행할거에요 -> 어떤 공격을 할지 고르는 DecideAttack을 } } } - // ════════════════════════════════════════ - // 🏃‍♂️ 무기 회수 로직 - // ════════════════════════════════════════ - - private void RetrieveWeaponLogic() - { - if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; - - agent.SetDestination(ironBall.transform.position); - agent.isStopped = false; - - if (animator != null) animator.SetFloat("Speed", agent.velocity.magnitude); - - float dist = Vector3.Distance(transform.position, ironBall.transform.position); - - if (dist <= 3.0f && !isAttacking) - { - StartCoroutine(PickUpBallRoutine()); - } - } - - private IEnumerator PickUpBallRoutine() - { - OnAttackStart(); - - if (agent != null && agent.isOnNavMesh) - { - agent.isStopped = true; - agent.velocity = Vector3.zero; - } - - if (animator != null) - { - animator.SetFloat("Speed", 0); - animator.Play("Skill_Pickup"); - } - - yield return new WaitForSeconds(0.8f); - - ironBall.transform.SetParent(handHolder); - ironBall.transform.localPosition = Vector3.zero; - ironBall.transform.localRotation = Quaternion.identity; - - if (ballRb != null) - { - ballRb.isKinematic = true; - ballRb.velocity = Vector3.zero; - } - - yield return new WaitForSeconds(1.0f); - - isWeaponless = false; - OnAttackEnd(); - - if (agent != null && agent.isOnNavMesh) agent.isStopped = false; - } - // ════════════════════════════════════════ // 🎬 전투 입장 // ════════════════════════════════════════ - public void StartBossBattle() + public void StartBossBattle() // 함수를 선언할거에요 -> 외부에서 전투를 시작시킬 수 있는 StartBossBattle을 { - if (isBattleStarted) return; - StartCoroutine(BattleStartRoutine()); + if (isBattleStarted) return; // 조건이 맞으면 중단할거에요 -> 이미 전투가 시작되었다면 중복 실행을 + StartCoroutine(BattleStartRoutine()); // 코루틴을 실행할거에요 -> Roar 연출 후 전투를 시작하는 BattleStartRoutine을 } - private IEnumerator BattleStartRoutine() + private IEnumerator BattleStartRoutine() // 코루틴 함수를 선언할거에요 -> Roar 연출과 전투 시작을 담당하는 BattleStartRoutine을 { - Debug.Log("🔥 보스 전투 시작! (Roar)"); + Debug.Log("🔥 [Boss] Roar 시작!"); // 로그를 출력할거에요 -> Roar가 시작되었다는 메시지를 + isPerformingAction = true; // 상태를 바꿀거에요 -> 연출 중 플래그를 참(true)으로 - // ⭐ [핵심] 연출 중 플래그 ON → ExecuteAILogic이 아무것도 안 함 - isPerformingAction = true; + if (bossHealthBar != null) bossHealthBar.SetActive(true); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에 켜기를 + if (counterSystem != null) counterSystem.InitializeBattle(); // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 있다면 전투 초기화를 - if (bossHealthBar != null) bossHealthBar.SetActive(true); - if (counterSystem != null) counterSystem.InitializeBattle(); + if (animator != null) animator.Play(anim_Roar, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar 애니메이션을 처음부터 재생을 - // ⭐ [핵심] Roar 재생 - if (animator != null) + yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> Roar 애니메이션이 끝날 2초의 시간을 + + if (animator != null) animator.Play(anim_Idle, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar가 끝났으니 대기 모션으로 전환을 + + isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 중 플래그를 거짓(false)으로 + isBattleStarted = true; // 상태를 바꿀거에요 -> 전투 시작 플래그를 참(true)으로 + + Debug.Log("⚔️ [Boss] Roar 종료 → 전투 루프 시작!"); // 로그를 출력할거에요 -> 전투 루프가 시작되었다는 메시지를 + + if (agent != null) // 조건이 맞으면 실행할거에요 -> 길찾기 에이전트가 존재한다면 { - animator.Play("Roar", 0, 0f); - } - - // Roar 애니메이션 대기 (길이에 맞게 조절) - yield return new WaitForSeconds(2.0f); - - // ⭐ [핵심] Roar 끝나면 Idle로 전환 - if (animator != null) - { - animator.Play("Monster_Idle", 0, 0f); - } - - // 연출 종료 → 전투 시작 - isPerformingAction = false; - isBattleStarted = true; - - // NavMeshAgent 활성화 - if (agent != null) - { - agent.enabled = true; - - // NavMesh 위에 올라갈 때까지 잠시 대기 - int retry = 0; - while (!agent.isOnNavMesh && retry < 10) + agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를 활성화(true)로 + int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로 + while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> NavMesh에 안 닿았고 10번 미만인 동안 { - retry++; - yield return new WaitForSeconds(0.1f); + retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼 + yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을 } - if (agent.isOnNavMesh) + if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 올라갔다면 { - agent.isStopped = false; + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 추격을 시작하라고 + Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 출력할거에요 -> NavMesh 연결 성공 메시지를 + } + else // 조건이 틀리면 실행할거에요 -> NavMesh에 못 올라갔다면 + { + Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 출력할거에요 -> 연결 실패 경고를 } } - - Debug.Log("⚔️ 보스 전투 루프 시작!"); } - private void LookAtTarget(Vector3 targetPos) + // ════════════════════════════════════════ + // 🚶 이동 애니메이션 & 타겟팅 + // ════════════════════════════════════════ + + private void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 상태에 맞는 애니메이션을 재생하는 UpdateMovementAnimation을 { - Vector3 dir = targetPos - transform.position; - dir.y = 0; - if (dir != Vector3.zero) + if (animator == null || isPerformingAction || isAttacking || isHit || isDead) return; // 조건이 맞으면 중단할거에요 -> 애니메이션을 바꿀 수 없는 상태라면 + + AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를 + + if (agent != null && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 현재 이동 속도가 0.1보다 크다면 { - transform.rotation = Quaternion.Slerp( - transform.rotation, - Quaternion.LookRotation(dir), - Time.deltaTime * 5f + if (!state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 아직 걷기 모션이 아니라면 + animator.Play(anim_Walk); // 애니메이션을 바꿀거에요 -> 걷기 모션으로 + } + else // 조건이 틀리면 실행할거에요 -> 거의 멈춰있다면 + { + if (!state.IsName(anim_Idle) && !state.IsName(anim_Roar)) // 조건이 맞으면 실행할거에요 -> 대기나 포효 모션이 아니라면 + animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로 + } + } + + private void LookAtTarget(Vector3 targetPos) // 함수를 선언할거에요 -> 타겟을 부드럽게 쳐다보는 LookAtTarget을 + { + Vector3 dir = targetPos - transform.position; // 벡터를 계산할거에요 -> 내 위치에서 타겟까지의 방향을 + dir.y = 0; // 값을 바꿀거에요 -> 위아래로 고개가 꺾이지 않게 y를 0으로 + if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 0이 아니라면 + { + transform.rotation = Quaternion.Slerp( // 회전시킬거에요 -> 현재 회전에서 타겟 방향으로 부드럽게 + transform.rotation, // 시작값으로 사용할거에요 -> 현재 회전값을 + Quaternion.LookRotation(dir), // 목표값으로 사용할거에요 -> 타겟을 향하는 회전값을 + Time.deltaTime * 5f // 속도를 지정할거에요 -> 프레임 시간의 5배 속도로 ); } } // ════════════════════════════════════════ - // 🧠 AI 공격 판단 + // 🧠 공격 판단 // ════════════════════════════════════════ - private void DecideAttack() + private void DecideAttack() // 함수를 선언할거에요 -> 어떤 공격 패턴을 쓸지 고르는 DecideAttack을 { - if (animator != null) animator.SetFloat("Speed", 0f); + if (target != null) LookAtTarget(target.position); // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 그쪽을 바라보기를 - // 공격 전 이동 멈춤 - if (agent != null && agent.isOnNavMesh) + string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 변수를 선언할거에요 -> 카운터 시스템에서 받은 패턴 이름을 patternName에 + Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 출력할거에요 -> 선택된 패턴 이름을 + + switch (patternName) // 분기할거에요 -> 받아온 패턴 이름에 따라서 { - agent.isStopped = true; - agent.velocity = Vector3.zero; - } - - // 플레이어를 바라봄 - if (target != null) LookAtTarget(target.position); - - string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; - - Debug.Log($"🧠 보스 패턴 선택: {patternName}"); - - switch (patternName) - { - case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; - case "Smash": StartCoroutine(Pattern_SmashAttack()); break; - case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; - default: StartCoroutine(Pattern_ThrowBall()); break; + case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 일치하면 실행할거에요 -> 돌진 공격을 + case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 일치하면 실행할거에요 -> 내려찍기 공격을 + case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 일치하면 실행할거에요 -> 휩쓸기 공격을 + default: StartCoroutine(Pattern_ThrowBall()); break; // 맞는게 없으면 실행할거에요 -> 공 던지기 공격을 } } // ════════════════════════════════════════ - // ⚔️ 공격 패턴 + // 🏃 무기 회수 // ════════════════════════════════════════ - // 1. 돌진 - private IEnumerator Pattern_DashAttack() + private void RetrieveWeaponLogic() // 함수를 선언할거에요 -> 던진 쇠공을 주우러 가는 RetrieveWeaponLogic을 { - OnAttackStart(); + if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 쇠공이 없거나 길찾기가 불가능하다면 - if (agent != null) agent.enabled = false; + agent.SetDestination(ironBall.transform.position); // 명령을 내릴거에요 -> 길찾기 목표를 쇠공의 위치로 + agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고 + UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걸어갈 때 이동 애니메이션이 나오도록 - if (animator != null) animator.Play("Skill_Dash_Ready", 0, 0f); - yield return new WaitForSeconds(0.5f); - - // 돌진 - if (rb != null) - { - rb.isKinematic = false; - rb.velocity = transform.forward * 20f; - } - - if (animator != null) animator.Play("Skill_Dash_Go", 0, 0f); - yield return new WaitForSeconds(1.0f); - - // 정지 - if (rb != null) - { - rb.velocity = Vector3.zero; - rb.isKinematic = true; - } - - // NavMeshAgent 복구 - if (agent != null && isBattleStarted) - { - agent.enabled = true; - // NavMesh 복귀 대기 - int retry = 0; - while (agent != null && !agent.isOnNavMesh && retry < 10) - { - retry++; - yield return new WaitForSeconds(0.1f); - } - } - - OnAttackEnd(); + if (Vector3.Distance(transform.position, ironBall.transform.position) <= 3.0f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 쇠공과 3m 이내이고 공격 중이 아니라면 + StartCoroutine(PickUpBallRoutine()); // 코루틴을 실행할거에요 -> 바닥의 쇠공을 줍는 PickUpBallRoutine을 } - // 2. 스매시 - private IEnumerator Pattern_SmashAttack() + private IEnumerator PickUpBallRoutine() // 코루틴 함수를 선언할거에요 -> 쇠공 줍기 행동인 PickUpBallRoutine을 { - OnAttackStart(); + OnAttackStart(); // 상태를 켤거에요 -> 다른 행동과 겹치지 않게 공격 플래그를 + if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 줍기 위해 이동을 멈추고 속도를 0으로 + if (animator != null) animator.Play(anim_Pickup); // 조건이 맞으면 실행할거에요 -> 줍는 애니메이션을 + yield return new WaitForSeconds(0.8f); // 기다릴거에요 -> 손이 바닥에 닿을 0.8초를 - if (animator != null) animator.Play("Skill_Smash_Impact", 0, 0f); - yield return new WaitForSeconds(1.2f); + ironBall.transform.SetParent(handHolder); // 부모를 바꿀거에요 -> 쇠공을 보스의 손 밑으로 + ironBall.transform.localPosition = Vector3.zero; // 위치를 바꿀거에요 -> 쇠공의 위치를 손의 정중앙으로 + ironBall.transform.localRotation = Quaternion.identity; // 회전을 바꿀거에요 -> 쇠공의 회전을 기본값으로 + if (ballRb != null) { ballRb.isKinematic = true; ballRb.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 쇠공의 물리를 끄고 속도를 0으로 - if (animator != null) animator.Play("Skill_Smash_Impact", 0, 0f); - yield return new WaitForSeconds(1.0f); - - OnAttackEnd(); - } - - // 3. 쓸기 - private IEnumerator Pattern_ShieldWall() - { - OnAttackStart(); - - if (animator != null) animator.Play("Skill_Sweep", 0, 0f); - yield return new WaitForSeconds(2.0f); - - OnAttackEnd(); - } - - // 4. 공 던지기 - private IEnumerator Pattern_ThrowBall() - { - OnAttackStart(); - - if (animator != null) animator.Play("Attack_Throw", 0, 0f); - yield return new WaitForSeconds(0.5f); - - if (ironBall != null && ballRb != null) - { - ironBall.transform.SetParent(null); - ballRb.isKinematic = false; - - Vector3 dir = (target.position - transform.position).normalized; - ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); - ballRb.angularDrag = 5f; - } - - isWeaponless = true; - - yield return new WaitForSeconds(1.0f); - OnAttackEnd(); + yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 일어서는 애니메이션이 끝날 1초를 + isWeaponless = false; // 상태를 바꿀거에요 -> 무기를 다시 쥐었으므로 맨손 상태를 거짓으로 + OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 처리를 + if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 다시 추격을 위해 이동 정지 해제를 } // ════════════════════════════════════════ - // ⭐ [신규] 피격 시 부모 클래스 StopAllCoroutines 대응 + // ✅ 공격 시작/종료 처리 (핵심) // ════════════════════════════════════════ - /// - /// 부모의 StartHit()이 StopAllCoroutines()를 호출하므로, - /// 보스 전용 상태를 여기서 복구합니다. - /// - protected override void OnStartHit() + public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 처리를 하는 OnAttackStart를 { - isPerformingAction = false; // 연출 중이었으면 해제 - - // 공격 중이었으면 해제 - if (isAttacking) - { - isAttacking = false; - if (myWeapon != null) myWeapon.DisableHitBox(); - } - - // 돌진 중 물리 복구 - if (rb != null && !rb.isKinematic) - { - rb.velocity = Vector3.zero; - rb.isKinematic = true; - } + isAttacking = true; // 상태를 바꿀거에요 -> 공격 중임을 참으로 + isPerformingAction = true; // 상태를 바꿀거에요 -> 다른 행동 불가 상태로 + if (agent != null) agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 } - /// - /// 피격 종료 후 NavMeshAgent 복구 - /// - public override void OnHitEnd() + public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를 { - base.OnHitEnd(); + StartCoroutine(RecoverRoutine()); // 코루틴을 시작할거에요 -> 후딜레이(회복) 처리를 위한 RecoverRoutine을 + } - // 전투 중이면 Agent 다시 활성화 - if (isBattleStarted && agent != null && !agent.enabled) - { - agent.enabled = true; - } + private IEnumerator RecoverRoutine() // 코루틴 함수를 선언할거에요 -> 공격 후 대기 시간을 갖는 RecoverRoutine을 + { + isAttacking = false; // 상태를 바꿀거에요 -> 공격이 끝났으므로 공격 중 상태를 거짓으로 + isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참으로 (이때는 추격 안 함) + + if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 대기 모션으로 전환을 + + // 딜레이 시간 (여기서 patternInterval 만큼 멍때림) + yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 패턴 간격 시간만큼 + + isResting = false; // 상태를 바꿀거에요 -> 휴식이 끝났으므로 거짓으로 + isPerformingAction = false; // 상태를 바꿀거에요 -> 이제 다시 다른 행동이 가능하도록 거짓으로 } // ════════════════════════════════════════ - // ⭐ [신규] 사망 처리 + // ⚔️ 공격 패턴들 // ════════════════════════════════════════ - protected override void OnDie() + // 패턴 1: 돌진 공격 + private IEnumerator Pattern_DashAttack() // 코루틴 함수를 선언할거에요 -> 돌진 패턴인 Pattern_DashAttack을 { - isBattleStarted = false; - isPerformingAction = false; - isWeaponless = false; + OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 + if (agent != null) agent.enabled = false; // 조건이 맞으면 실행할거에요 -> 돌진 시 물리력을 위해 길찾기를 끄기로 - // 물리 정리 - if (rb != null) + if (animator != null) animator.Play(anim_DashReady, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 준비 모션을 + yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 준비 포즈를 취할 0.5초를 + + if (rb != null) { rb.isKinematic = false; rb.velocity = transform.forward * 20f; } // 조건이 맞으면 실행할거에요 -> 물리를 켜고 앞으로 속도 20으로 돌진을 + if (animator != null) animator.Play(anim_DashGo, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 애니메이션을 + yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 돌진이 끝날 1초를 + + if (rb != null) { rb.velocity = Vector3.zero; rb.isKinematic = true; } // 조건이 맞으면 실행할거에요 -> 속도를 0으로 멈추고 물리를 끄기로 + if (agent != null && isBattleStarted) agent.enabled = true; // 조건이 맞으면 실행할거에요 -> 전투 중이면 길찾기를 다시 켜기로 + OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 + } + + // 패턴 2: 내려찍기 + private IEnumerator Pattern_SmashAttack() // 코루틴 함수를 선언할거에요 -> 내려찍기 패턴인 Pattern_SmashAttack을 + { + OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 + if (animator != null) animator.Play(anim_SmashCharge, 0, 0f); // 조건이 맞으면 실행할거에요 -> 기를 모으는 모션을 + yield return new WaitForSeconds(1.2f); // 기다릴거에요 -> 기를 모을 1.2초를 + if (animator != null) animator.Play(anim_SmashImpact, 0, 0f); // 조건이 맞으면 실행할거에요 -> 내려찍는 모션을 + yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 자세를 가다듬을 1초를 + OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 + } + + // 패턴 3: 휩쓸기 + private IEnumerator Pattern_ShieldWall() // 코루틴 함수를 선언할거에요 -> 휩쓸기 패턴인 Pattern_ShieldWall을 + { + OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 + if (animator != null) animator.Play(anim_Sweep, 0, 0f); // 조건이 맞으면 실행할거에요 -> 휩쓰는 모션을 + yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> 휘두르기가 끝날 2초를 + OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 + } + + // 패턴 4: 공 던지기 + private IEnumerator Pattern_ThrowBall() // 코루틴 함수를 선언할거에요 -> 쇠공 던지기 패턴인 Pattern_ThrowBall을 + { + OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 + if (animator != null) animator.Play(anim_Throw, 0, 0f); // 조건이 맞으면 실행할거에요 -> 던지는 모션을 + yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 공을 던지기 직전인 0.5초를 + + if (ironBall != null && ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공과 물리 엔진이 존재한다면 { - rb.velocity = Vector3.zero; - rb.isKinematic = true; + ironBall.transform.SetParent(null); // 부모를 바꿀거에요 -> 쇠공을 손에서 완전히 분리로 + ballRb.isKinematic = false; // 기능을 켤거에요 -> 쇠공의 물리 연산을 활성화로 + Vector3 dir = (target.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을 + ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); // 힘을 가할거에요 -> 앞으로 20, 위로 5의 포물선 힘을 + ballRb.angularDrag = 5f; // 마찰을 줄거에요 -> 쇠공이 멀리 안 굴러가게 회전 마찰 5를 } - // 체력바 숨기기 - if (bossHealthBar != null) bossHealthBar.SetActive(false); + isWeaponless = true; // 상태를 바꿀거에요 -> 던졌으므로 맨손 상태를 참으로 + yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 던지고 자세를 가다듬을 1초를 + OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 + } + + // ════════════════════════════════════════ + // 💥 피격 / 사망 + // ════════════════════════════════════════ + + protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 보스가 맞았을 때 호출되는 OnStartHit을 + { + // 보스는 피격되어도 행동이 끊기지 않습니다 (슈퍼아머) + // 만약 끊기게 하려면 여기서 isPerformingAction = false; + } + + protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 보스 체력이 0이 되면 호출되는 OnDie를 + { + isBattleStarted = false; // 상태를 바꿀거에요 -> 전투가 끝났으므로 전투 플래그를 거짓으로 + isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 플래그를 거짓으로 + if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를 + GiveBossXP(); // 함수를 실행할거에요 -> 경험치를 주는 GiveBossXP를 + base.OnDie(); // 부모 클래스의 사망 처리를 호출할거에요 -> 애니메이션, 시체 제거 등 + } + + private void GiveBossXP() // 함수를 선언할거에요 -> 보스 처치 보상을 주는 GiveBossXP를 + { + Debug.Log("[BossController] 보스 처치 시도 중..."); // 로그를 출력할거에요 -> 보스 처치 시도 메시지를 + if (ObsessionSystem.instance != null) // 조건이 맞으면 실행할거에요 -> 경험치 시스템이 존재한다면 + ObsessionSystem.instance.AddRunXP(100); // 함수를 실행할거에요 -> 경험치 100을 주는 AddRunXP를 + else // 조건이 틀리면 실행할거에요 -> 시스템을 못 찾았다면 + Debug.LogError("[BossController] ObsessionSystem 인스턴스를 찾을 수 없습니다!"); // 에러를 출력할거에요 -> 시스템 누락 경고를 + Debug.Log("[BossController] 보스 처치 처리 완료!"); // 로그를 출력할거에요 -> 빨간 글씨로 처치 완료 메시지를 } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs b/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs index 2d4677dc..1c5ee9ea 100644 --- a/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs +++ b/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs @@ -1,29 +1,29 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class BossZoneTrigger : MonoBehaviour +public class BossZoneTrigger : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossZoneTrigger를 { - [SerializeField] private NorcielBoss boss; // - [SerializeField] private GameObject fogWall; // Ա Ȱ () + [SerializeField] private NorcielBoss boss; // 변수를 선언할거에요 -> 깨울 보스 스크립트(NorcielBoss)를 boss에 + [SerializeField] private GameObject fogWall; // 변수를 선언할거에요 -> 입구를 막을 안개벽 오브젝트를 fogWall에 - private bool hasTriggered = false; + private bool hasTriggered = false; // 변수를 초기화할거에요 -> 이미 발동했는지 여부를 거짓(false)으로 - private void OnTriggerEnter(Collider other) + private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 무언가가 트리거에 닿았을 때 OnTriggerEnter를 { - // ̹ ߵ߰ų, ÷̾ ƴϸ - if (hasTriggered) return; + // 이미 발동했거나, 플레이어가 아니면 무시 + if (hasTriggered) return; // 조건이 맞으면 중단할거에요 -> 이미 발동했다면(hasTriggered가 참이면) - if (other.CompareTag("Player")) + if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 닿은 물체의 태그가 "Player"라면 { - hasTriggered = true; + hasTriggered = true; // 상태를 바꿀거에요 -> 발동 상태를 참(true)으로 - // 1. - if (boss != null) boss.StartBossBattle(); + // 1. 보스 깨우기 + if (boss != null) boss.StartBossBattle(); // 조건이 맞으면 실행할거에요 -> 보스가 연결되어 있다면 전투 시작 함수(StartBossBattle)를 - // 2. Ա () - if (fogWall != null) fogWall.SetActive(true); + // 2. 도망 못 가게 입구 막기 (선택) + if (fogWall != null) fogWall.SetActive(true); // 조건이 맞으면 실행할거에요 -> 안개벽이 연결되어 있다면 활성화(true)를 - // 3. ƮŴ - // gameObject.SetActive(false); // ٷ Ȱ + // 3. 이 트리거는 할 일 다 했으니 끄기 + // gameObject.SetActive(false); // 바로 끄면 안개도 꺼질 수 있으니 주의 } } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/BossAI/CounterType.cs b/Assets/Scripts/Enemy/BossAI/CounterType.cs index cab44c60..5f60e7e9 100644 --- a/Assets/Scripts/Enemy/BossAI/CounterType.cs +++ b/Assets/Scripts/Enemy/BossAI/CounterType.cs @@ -1,10 +1,10 @@ -/// -/// 보스 카운터 패턴 타입 열거형 -/// -public enum CounterType -{ - None, - Dodge, // 회피 남발 카운터 - Aim, // 정조준 과다 카운터 - Pierce // 관통 화살 편중 카운터 -} +/// // 요약 설명을 적을거에요 -> 아래 enum이 뭔지 +/// 보스 카운터 패턴 타입 열거형 // 설명할거에요 -> 보스가 어떤 카운터 패턴을 쓰는지 구분하는 타입 +/// // 요약 설명을 끝낼거에요 -> 주석 블록을 +public enum CounterType // 열거형을 선언할거에요 -> CounterType이라는 이름의 enum을 +{ // 코드 블록을 시작할거에요 -> CounterType 범위를 + None, // 값을 정의할거에요 -> 카운터 없음 상태를 + Dodge, // 값을 정의할거에요 -> 회피 남발 카운터 타입을 + Aim, // 값을 정의할거에요 -> 정조준 과다 카운터 타입을 + Pierce // 값을 정의할거에요 -> 관통 화살 편중 카운터 타입을 +} // 코드 블록을 끝낼거에요 -> CounterType을 \ No newline at end of file diff --git a/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs b/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs index fd21dbfe..917e48a8 100644 --- a/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs +++ b/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs @@ -1,4 +1,4 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 /// /// 하이브리드 몬스터 스포너 (생성은 스포너 기준, 생존은 몬스터 기준) @@ -6,52 +6,52 @@ /// - Life & Hibernate: 몬스터 기준 거리로 최적화 /// - Wake Up: 범위 안으로 복귀 시 재활성화 /// -public class MonsterSpawner : MonoBehaviour +public class MonsterSpawner : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterSpawner를 { - [Header("=== 생성 설정 (Birth - 스포너 기준) ===")] - [Tooltip("스포너로부터 이 거리 안에 플레이어가 들어오면 몬스터 생성")] - [SerializeField] private float spawnRange = 25f; + [Header("=== 생성 설정 (Birth - 스포너 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 생성 설정 (Birth - 스포너 기준) === 을 + [Tooltip("스포너로부터 이 거리 안에 플레이어가 들어오면 몬스터 생성")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float spawnRange = 25f; // 변수를 선언할거에요 -> 몬스터 생성 거리(25.0)를 spawnRange에 - [Tooltip("몬스터가 죽은 후 재생성까지 대기 시간")] - [SerializeField] private float respawnCooldown = 3f; + [Tooltip("몬스터가 죽은 후 재생성까지 대기 시간")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float respawnCooldown = 3f; // 변수를 선언할거에요 -> 재소환 대기시간(3초)을 respawnCooldown에 - [Tooltip("오브젝트 풀에서 가져올 몬스터 태그")] - [SerializeField] private string mobTag = "NormalMob"; + [Tooltip("오브젝트 풀에서 가져올 몬스터 태그")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private string mobTag = "NormalMob"; // 변수를 선언할거에요 -> 소환할 몬스터의 태그 이름("NormalMob")을 mobTag에 - [Header("=== 최적화 설정 (Life & Hibernate - 몬스터 기준) ===")] - [Tooltip("몬스터 본체로부터 이 거리를 벗어나면 강제 동면 (전투 중이어도!)")] - [SerializeField] private float optimizationRange = 60f; + [Header("=== 최적화 설정 (Life & Hibernate - 몬스터 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 최적화 설정 (Life & Hibernate - 몬스터 기준) === 을 + [Tooltip("몬스터 본체로부터 이 거리를 벗어나면 강제 동면 (전투 중이어도!)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float optimizationRange = 60f; // 변수를 선언할거에요 -> 몬스터 최적화(동면) 거리(60.0)를 optimizationRange에 // 내부 상태 - private GameObject _myMonster; - private MonsterClass _monsterScript; - private Transform _player; - private float _nextSpawnTime; + private GameObject _myMonster; // 변수를 선언할거에요 -> 소환된 몬스터 게임오브젝트를 담을 _myMonster를 + private MonsterClass _monsterScript; // 변수를 선언할거에요 -> 몬스터의 스크립트(MonsterClass)를 담을 _monsterScript를 + private Transform _player; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 _player를 + private float _nextSpawnTime; // 변수를 선언할거에요 -> 다음 소환 가능 시간을 저장할 _nextSpawnTime을 - private void Start() + private void Start() // 함수를 실행할거에요 -> 게임 시작 시 호출되는 Start를 { - FindPlayer(); + FindPlayer(); // 함수를 실행할거에요 -> 플레이어를 찾는 FindPlayer를 } - private void FindPlayer() + private void FindPlayer() // 함수를 선언할거에요 -> 플레이어를 찾아 변수에 저장하는 FindPlayer를 { - GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); - if (playerObj != null) _player = playerObj.transform; + GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); // 오브젝트를 찾을거에요 -> "Player" 태그가 붙은 오브젝트를 playerObj에 + if (playerObj != null) _player = playerObj.transform; // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면 그 위치 정보를 _player에 저장 } - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 { - if (_player == null) { FindPlayer(); return; } + if (_player == null) { FindPlayer(); return; } // 조건이 맞으면 실행할거에요 -> 플레이어 정보가 없다면 다시 찾고 이번 프레임은 중단(return) // 몬스터가 없거나 죽었으면 -> Birth - if (_myMonster == null || (_monsterScript != null && _monsterScript.IsDead)) + if (_myMonster == null || (_monsterScript != null && _monsterScript.IsDead)) // 조건이 맞으면 실행할거에요 -> 몬스터가 없거나, 몬스터 스크립트가 있는데 죽은 상태라면 { - HandleBirth(); + HandleBirth(); // 함수를 실행할거에요 -> 몬스터 생성 로직인 HandleBirth를 } // 몬스터가 살아있으면 -> Life & Hibernate - else + else // 조건이 틀리면 실행할거에요 -> 몬스터가 살아서 활동 중이라면 { - HandleLifeAndHibernate(); + HandleLifeAndHibernate(); // 함수를 실행할거에요 -> 생존 및 최적화(동면) 로직인 HandleLifeAndHibernate를 } } @@ -59,25 +59,25 @@ public class MonsterSpawner : MonoBehaviour // 🐣 BIRTH (생성) - 스포너 기준 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - private void HandleBirth() + private void HandleBirth() // 함수를 선언할거에요 -> 몬스터 생성 조건을 확인하는 HandleBirth를 { // ⭐ 스포너 <-> 플레이어 거리 - float distanceFromSpawner = Vector3.Distance(transform.position, _player.position); + float distanceFromSpawner = Vector3.Distance(transform.position, _player.position); // 값을 계산할거에요 -> 스포너와 플레이어 사이의 거리를 distanceFromSpawner에 - if (distanceFromSpawner <= spawnRange && Time.time >= _nextSpawnTime) + if (distanceFromSpawner <= spawnRange && Time.time >= _nextSpawnTime) // 조건이 맞으면 실행할거에요 -> 거리가 생성 범위 이내이고, 쿨타임 시간이 지났다면 { - SpawnMonster(); + SpawnMonster(); // 함수를 실행할거에요 -> 실제 몬스터를 소환하는 SpawnMonster를 } } - private void SpawnMonster() + private void SpawnMonster() // 함수를 선언할거에요 -> 오브젝트 풀에서 몬스터를 가져오는 SpawnMonster를 { - _myMonster = GenericObjectPool.Instance.SpawnFromPool(mobTag, transform.position, transform.rotation); + _myMonster = GenericObjectPool.Instance.SpawnFromPool(mobTag, transform.position, transform.rotation); // 오브젝트를 가져올거에요 -> 풀(Pool)에서 mobTag에 맞는 몬스터를 스포너 위치에 소환해서 _myMonster에 - if (_myMonster != null) + if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 소환에 성공했다면 { - _monsterScript = _myMonster.GetComponent(); - if (_monsterScript != null) _monsterScript.ResetStats(); + _monsterScript = _myMonster.GetComponent(); // 컴포넌트를 가져올거에요 -> 몬스터의 핵심 스크립트(MonsterClass)를 _monsterScript에 + if (_monsterScript != null) _monsterScript.ResetStats(); // 조건이 맞으면 실행할거에요 -> 스크립트가 있다면 몬스터의 상태(체력 등)를 초기화(ResetStats) } } @@ -85,43 +85,43 @@ public class MonsterSpawner : MonoBehaviour // 💤 LIFE & HIBERNATE (생존/동면) - 몬스터 기준 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - private void HandleLifeAndHibernate() + private void HandleLifeAndHibernate() // 함수를 선언할거에요 -> 몬스터의 활동 및 최적화를 관리하는 HandleLifeAndHibernate를 { - if (_myMonster == null) { _monsterScript = null; return; } + if (_myMonster == null) { _monsterScript = null; return; } // 조건이 맞으면 실행할거에요 -> 몬스터 오브젝트가 사라졌다면 스크립트 변수도 비우고 중단(return) // ⭐ 몬스터 본체 <-> 플레이어 거리 - float distanceFromMonster = Vector3.Distance(_myMonster.transform.position, _player.position); + float distanceFromMonster = Vector3.Distance(_myMonster.transform.position, _player.position); // 값을 계산할거에요 -> 몬스터와 플레이어 사이의 거리를 distanceFromMonster에 - if (distanceFromMonster > optimizationRange) + if (distanceFromMonster > optimizationRange) // 조건이 맞으면 실행할거에요 -> 몬스터와 플레이어 거리가 최적화 범위(optimizationRange)보다 멀다면 { // 😴 HIBERNATE (동면) - HibernateMonster(); + HibernateMonster(); // 함수를 실행할거에요 -> 몬스터를 잠재우는 HibernateMonster를 } - else + else // 조건이 틀리면 실행할거에요 -> 거리가 가깝다면 { // 👁 WAKE UP (기상) - WakeUpMonster(); + WakeUpMonster(); // 함수를 실행할거에요 -> 몬스터를 깨우는 WakeUpMonster를 } } - private void HibernateMonster() + private void HibernateMonster() // 함수를 선언할거에요 -> 몬스터를 비활성화하는 HibernateMonster를 { - if (_myMonster.activeSelf) + if (_myMonster.activeSelf) // 조건이 맞으면 실행할거에요 -> 몬스터가 현재 켜져 있는(Active) 상태라면 { - _myMonster.SetActive(false); + _myMonster.SetActive(false); // 기능을 끌거에요 -> 몬스터 오브젝트를 비활성화(false)로 } } - private void WakeUpMonster() + private void WakeUpMonster() // 함수를 선언할거에요 -> 몬스터를 다시 활성화하는 WakeUpMonster를 { - if (!_myMonster.activeSelf) + if (!_myMonster.activeSelf) // 조건이 맞으면 실행할거에요 -> 몬스터가 현재 꺼져 있는(Inactive) 상태라면 { - _myMonster.SetActive(true); + _myMonster.SetActive(true); // 기능을 켤거에요 -> 몬스터 오브젝트를 활성화(true)로 - if (_monsterScript != null) + if (_monsterScript != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 존재한다면 { // ⭐ AI 복구 (전투 중이었다면 추적 재개) - _monsterScript.Reactivate(); + _monsterScript.Reactivate(); // 함수를 실행할거에요 -> 몬스터의 AI를 다시 작동시키는 Reactivate를 } } } @@ -133,31 +133,31 @@ public class MonsterSpawner : MonoBehaviour /// /// 몬스터가 죽었을 때 호출 (쿨타임 적용) /// - public void NotifyMonsterDead() + public void NotifyMonsterDead() // 함수를 선언할거에요 -> 외부에서 몬스터 사망을 알릴 때 쓰는 NotifyMonsterDead를 { // 쿨타임 계산: 현재 시간 + 대기 시간 - _nextSpawnTime = Time.time + respawnCooldown; + _nextSpawnTime = Time.time + respawnCooldown; // 값을 저장할거에요 -> 현재 시간에 쿨타임을 더해서 다음 소환 시간(_nextSpawnTime)으로 // 몬스터 참조를 비워서 Update에서 다시 HandleBirth()로 넘어가게 함 - _myMonster = null; - _monsterScript = null; + _myMonster = null; // 값을 비울거에요 -> 소환된 몬스터 변수를 null로 + _monsterScript = null; // 값을 비울거에요 -> 몬스터 스크립트 변수를 null로 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Gizmos // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - private void OnDrawGizmosSelected() + private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 오브젝트 선택 시 기즈모를 그리는 OnDrawGizmosSelected를 { // 스포너 생성 범위 (빨간색) - Gizmos.color = Color.red; - Gizmos.DrawWireSphere(transform.position, spawnRange); + Gizmos.color = Color.red; // 색상을 설정할거에요 -> 기즈모 색을 빨간색으로 + Gizmos.DrawWireSphere(transform.position, spawnRange); // 그림을 그릴거에요 -> 스포너 위치에 생성 범위(spawnRange) 크기의 원을 // 몬스터 최적화 범위 (파란색) - if (_myMonster != null) + if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 소환된 몬스터가 있다면 { - Gizmos.color = Color.blue; - Gizmos.DrawWireSphere(_myMonster.transform.position, optimizationRange); + Gizmos.color = Color.blue; // 색상을 설정할거에요 -> 기즈모 색을 파란색으로 + Gizmos.DrawWireSphere(_myMonster.transform.position, optimizationRange); // 그림을 그릴거에요 -> 몬스터 위치에 최적화 범위(optimizationRange) 크기의 원을 } } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs b/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs index 2af849c2..90d01c5d 100644 --- a/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs +++ b/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs @@ -1,24 +1,24 @@ -using System.Collections.Generic; -using UnityEngine; +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class MobUpdateManager : MonoBehaviour +public class MobUpdateManager : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MobUpdateManager를 { - public static MobUpdateManager Instance; + public static MobUpdateManager Instance; // 변수를 선언할거에요 -> 어디서든 접근 가능한 싱글톤 인스턴스 Instance를 // ⭐ Monster -> MonsterClass로 수정하여 에러 해결 - private List _activeMobs = new List(); + private List _activeMobs = new List(); // 리스트를 초기화할거에요 -> 활성화된 몬스터들을 담을 _activeMobs를 - private void Awake() => Instance = this; + private void Awake() => Instance = this; // 함수를 실행할거에요 -> 스크립트 시작 시 내 자신(this)을 Instance에 넣기를 // ⭐ 매개변수 타입도 MonsterClass로 변경 - public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob); - public void UnregisterMob(MonsterClass mob) => _activeMobs.Remove(mob); + public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에 추가하는 RegisterMob을 + public void UnregisterMob(MonsterClass mob) => _activeMobs.Remove(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에서 제거하는 UnregisterMob을 - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 { - for (int i = 0; i < _activeMobs.Count; i++) + for (int i = 0; i < _activeMobs.Count; i++) // 반복할거에요 -> 리스트에 있는 모든 몬스터 개수만큼 { // 리스트를 돌며 카메라 최적화 로직이 담긴 OnManagedUpdate 호출 - if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate(); + if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate(); // 조건이 맞으면 실행할거에요 -> 몬스터가 존재한다면 관리 업데이트 함수(OnManagedUpdate)를 } } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/Types/DummyBot.cs b/Assets/Scripts/Enemy/Types/DummyBot.cs index 88ef8e9c..4ad5b5f8 100644 --- a/Assets/Scripts/Enemy/Types/DummyBot.cs +++ b/Assets/Scripts/Enemy/Types/DummyBot.cs @@ -1,35 +1,37 @@ -using UnityEngine; -using System; // ⭐ Action 사용을 위해 필요 +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 +using System; // Action 이벤트를 쓰기 위해 불러올거에요 -> System을 -public class TrainingDummy : MonoBehaviour, IDamageable -{ - [Header("더미 설정")] - [SerializeField] private float maxHP = 9999f; - private float _currentHP; +public class TrainingDummy : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> 더미 오브젝트가 데미지를 받게 만들 TrainingDummy를 +{ // 코드 블록을 시작할거에요 -> TrainingDummy 범위를 - private Animator _animator; + [Header("더미 설정")] // 인스펙터에 제목을 표시할거에요 -> 더미 설정을 + [SerializeField] private float maxHP = 9999f; // 변수를 선언할거에요 -> 최대 체력을 9999로 설정하는 maxHP를 + private float _currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHP를 - // ⭐ UI가 이 신호를 듣고 게이지를 줄입니다. - public event Action OnHealthChanged; + private Animator _animator; // 변수를 선언할거에요 -> 애니메이터 컴포넌트를 담을 _animator를 - private void Start() - { - _currentHP = maxHP; - _animator = GetComponent(); - } + // ⭐ UI가 이 신호를 듣고 게이지를 줄입니다. // 설명을 적을거에요 -> UI 업데이트용 이벤트임을 + public event Action OnHealthChanged; // 이벤트를 선언할거에요 -> (현재체력, 최대체력) 알림을 보내는 OnHealthChanged를 - public void TakeDamage(float amount) - { - _currentHP -= amount; + private void Start() // 함수를 선언할거에요 -> 시작할 때 1번 실행되는 Start를 + { // 코드 블록을 시작할거에요 -> Start 범위를 + _currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로 초기화 + _animator = GetComponent(); // 컴포넌트를 가져올거에요 -> Animator를 찾아 _animator에 + } // 코드 블록을 끝낼거에요 -> Start를 - // ⭐ UI에 현재 체력과 최대 체력 정보를 보냅니다. - OnHealthChanged?.Invoke(_currentHP, maxHP); + public void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 주면 호출되는 TakeDamage를 + { // 코드 블록을 시작할거에요 -> TakeDamage 범위를 + _currentHP -= amount; // 값을 뺄거에요 -> 현재 체력에서 데미지만큼 감소 - Debug.Log($"[더미] 피격! 데미지: {amount} | 남은 체력: {_currentHP}"); + // ⭐ UI에 현재 체력과 최대 체력 정보를 보냅니다. // 설명을 적을거에요 -> UI 갱신 트리거임을 + OnHealthChanged?.Invoke(_currentHP, maxHP); // 이벤트를 호출할거에요 -> UI에게 체력 변화 전달 - if (_animator != null) - { - _animator.Play("Hit", 0, 0f); - } - } -} \ No newline at end of file + Debug.Log($"[더미] 피격! 데미지: {amount} | 남은 체력: {_currentHP}"); // 로그를 찍을거에요 -> 데미지/남은 체력 확인용 + + if (_animator != null) // 조건을 검사할거에요 -> 애니메이터가 있는지 + { // 코드 블록을 시작할거에요 -> 애니메이션 재생 처리 + _animator.Play("Hit", 0, 0f); // 애니메이션을 재생할거에요 -> Hit 상태를 0번 레이어에서 처음부터 + } // 코드 블록을 끝낼거에요 -> 애니메이션 처리 + } // 코드 블록을 끝낼거에요 -> TakeDamage를 + +} // 코드 블록을 끝낼거에요 -> TrainingDummy를 \ No newline at end of file diff --git a/Assets/Scripts/Items/Pickups/HealthPotion.cs b/Assets/Scripts/Items/Pickups/HealthPotion.cs index 2375dc09..a6b16817 100644 --- a/Assets/Scripts/Items/Pickups/HealthPotion.cs +++ b/Assets/Scripts/Items/Pickups/HealthPotion.cs @@ -1,18 +1,20 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class HealthPotion : MonoBehaviour -{ - [Header("--- ---")] - [SerializeField] private float healAmount = 30f; // ȸ +public class HealthPotion : MonoBehaviour // 클래스를 선언할거에요 -> 체력 포션 아이템인 HealthPotion을 +{ // 코드 블록을 시작할거에요 -> HealthPotion 범위를 - // PlayerInteraction ȣ Լ - public void Use(PlayerHealth target) - { - if (target == null) return; + [Header("--- 포션 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> --- 포션 설정 --- 을 + [SerializeField] private float healAmount = 30f; // 변수를 선언할거에요 -> 회복량(30)을 healAmount에 - target.Heal(healAmount); // ÷̾ ü ȸ - Debug.Log($"[Potion] ü {healAmount} ȸ!"); + // PlayerInteraction에서 호출할 함수 // 설명을 적을거에요 -> 상호작용 스크립트가 이걸 실행한다는 걸 + public void Use(PlayerHealth target) // 함수를 선언할거에요 -> 대상 플레이어를 회복시키는 Use를 + { // 코드 블록을 시작할거에요 -> Use 범위를 + if (target == null) return; // 조건이 맞으면 종료할거에요 -> 대상이 없으면 아무것도 안 함 - Destroy(gameObject); // - } -} \ No newline at end of file + target.Heal(healAmount); // 함수를 실행할거에요 -> 플레이어 체력을 healAmount 만큼 회복 + Debug.Log($"[Potion] 체력 {healAmount} 회복!"); // 로그를 찍을거에요 -> 회복 로그 출력 + + Destroy(gameObject); // 오브젝트를 삭제할거에요 -> 사용한 포션 아이템 제거 + } // 코드 블록을 끝낼거에요 -> Use를 + +} // 코드 블록을 끝낼거에요 -> HealthPotion을 \ No newline at end of file diff --git a/Assets/Scripts/Items/VFX/ItemHighlight.cs b/Assets/Scripts/Items/VFX/ItemHighlight.cs index b4bf942a..79576057 100644 --- a/Assets/Scripts/Items/VFX/ItemHighlight.cs +++ b/Assets/Scripts/Items/VFX/ItemHighlight.cs @@ -1,63 +1,65 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class ItemHighlight : MonoBehaviour -{ - [Header("--- 참조 ---")] - [SerializeField] private MeshRenderer weaponRenderer; +public class ItemHighlight : MonoBehaviour // 클래스를 선언할거에요 -> 아이템 하이라이트(발광 깜빡임) 담당 ItemHighlight를 +{ // 코드 블록을 시작할거에요 -> ItemHighlight 범위를 - [Header("--- 하이라이트 설정 ---")] - [ColorUsage(true, true)] - [SerializeField] private Color highlightColor = Color.yellow; - [SerializeField] private float blinkSpeed = 2f; - [SerializeField] private float minIntensity = 0.5f; - [SerializeField] private float maxIntensity = 3.0f; + [Header("--- 참조 ---")] // 인스펙터에 제목을 표시할거에요 -> 참조 섹션을 + [SerializeField] private MeshRenderer weaponRenderer; // 변수를 선언할거에요 -> 하이라이트 줄 렌더러를 weaponRenderer에 - private Material _material; - private static readonly int EmissionProperty = Shader.PropertyToID("_EmissionColor"); + [Header("--- 하이라이트 설정 ---")] // 인스펙터에 제목을 표시할거에요 -> 하이라이트 설정 섹션을 + [ColorUsage(true, true)] // 속성을 붙일거에요 -> HDR 색상도 고를 수 있게 + [SerializeField] private Color highlightColor = Color.yellow; // 변수를 선언할거에요 -> 발광 색상을 노란색으로 + [SerializeField] private float blinkSpeed = 2f; // 변수를 선언할거에요 -> 깜빡이는 속도를 2로 + [SerializeField] private float minIntensity = 0.5f; // 변수를 선언할거에요 -> 최소 발광 세기를 0.5로 + [SerializeField] private float maxIntensity = 3.0f; // 변수를 선언할거에요 -> 최대 발광 세기를 3.0으로 - private void Awake() - { - if (weaponRenderer == null) weaponRenderer = GetComponentInChildren(); + private Material _material; // 변수를 선언할거에요 -> 렌더러의 머티리얼을 담을 _material을 + private static readonly int EmissionProperty = Shader.PropertyToID("_EmissionColor"); // 값을 캐싱할거에요 -> _EmissionColor 프로퍼티 ID를 - if (weaponRenderer != null) - { - _material = weaponRenderer.material; - _material.EnableKeyword("_EMISSION"); - } - } + private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + if (weaponRenderer == null) weaponRenderer = GetComponentInChildren(); // 조건이 맞으면 실행할거에요 -> 렌더러가 비어있으면 자식에서 자동으로 찾기 - // ⭐ [추가] 부모 오브젝트(손)가 바뀌었을 때 실행됩니다. - private void OnTransformParentChanged() - { - // 부모가 없다면 = 바닥에 떨어졌다면 - if (transform.parent == null) - { - this.enabled = true; // 스크립트를 다시 깨웁니다. - } - } + if (weaponRenderer != null) // 조건을 검사할거에요 -> 렌더러를 찾았는지 + { // 코드 블록을 시작할거에요 -> 렌더러가 있을 때 처리 + _material = weaponRenderer.material; // 머티리얼을 가져올거에요 -> 렌더러의 material을 _material에 + _material.EnableKeyword("_EMISSION"); // 키워드를 켤거에요 -> 발광(Emission) 기능을 활성화 + } // 코드 블록을 끝낼거에요 -> 렌더러 처리 + } // 코드 블록을 끝낼거에요 -> Awake를 - private void Update() - { - if (_material == null) return; + // ⭐ [추가] 부모 오브젝트(손)가 바뀌었을 때 실행됩니다. // 설명을 적을거에요 -> 부모 변경 이벤트 훅임을 + private void OnTransformParentChanged() // 함수를 선언할거에요 -> 부모가 바뀌면 자동 호출되는 OnTransformParentChanged를 + { // 코드 블록을 시작할거에요 -> OnTransformParentChanged 범위를 + // 부모가 없다면 = 바닥에 떨어졌다면 // 설명을 적을거에요 -> 다시 주워지지 않은 상태라는 뜻 + if (transform.parent == null) // 조건을 검사할거에요 -> 부모가 없는지 + { // 코드 블록을 시작할거에요 -> 바닥에 떨어진 경우 처리 + this.enabled = true; // 스크립트를 켤거에요 -> Update가 다시 돌게 + } // 코드 블록을 끝낼거에요 -> 바닥 처리 + } // 코드 블록을 끝낼거에요 -> OnTransformParentChanged를 - // 누군가 주운 상태(부모가 있음)라면 빛을 끄고 퇴근합니다. - if (transform.parent != null) - { - StopHighlight(); - return; - } + private void Update() // 함수를 선언할거에요 -> 매 프레임 실행되는 Update를 + { // 코드 블록을 시작할거에요 -> Update 범위를 + if (_material == null) return; // 조건이 맞으면 종료할거에요 -> 머티리얼이 없으면 처리 불가 - float lerpValue = Mathf.PingPong(Time.time * blinkSpeed, 1f); - float intensity = Mathf.Lerp(minIntensity, maxIntensity, lerpValue); - _material.SetColor(EmissionProperty, highlightColor * intensity); - } + // 누군가 주운 상태(부모가 있음)라면 빛을 끄고 퇴근합니다. // 설명을 적을거에요 -> 손에 들린 상태면 하이라이트 중단 + if (transform.parent != null) // 조건을 검사할거에요 -> 부모가 있는지(주워졌는지) + { // 코드 블록을 시작할거에요 -> 주워진 경우 처리 + StopHighlight(); // 함수를 실행할거에요 -> 하이라이트 끄기 + return; // 함수를 끝낼거에요 -> 아래 발광 계산은 안 하게 + } // 코드 블록을 끝낼거에요 -> 주워진 처리 - public void StopHighlight() - { - if (_material != null) - { - _material.SetColor(EmissionProperty, Color.black); - this.enabled = false; // Update를 멈춥니다. - } - } -} \ No newline at end of file + float lerpValue = Mathf.PingPong(Time.time * blinkSpeed, 1f); // 값을 계산할거에요 -> 0~1로 왕복하는 값(깜빡임용)을 + float intensity = Mathf.Lerp(minIntensity, maxIntensity, lerpValue); // 값을 계산할거에요 -> 최소~최대 사이 발광 세기를 + _material.SetColor(EmissionProperty, highlightColor * intensity); // 값을 설정할거에요 -> 발광 색상을 (색 * 세기)로 + } // 코드 블록을 끝낼거에요 -> Update를 + + public void StopHighlight() // 함수를 선언할거에요 -> 하이라이트를 끄는 StopHighlight를 + { // 코드 블록을 시작할거에요 -> StopHighlight 범위를 + if (_material != null) // 조건을 검사할거에요 -> 머티리얼이 있는지 + { // 코드 블록을 시작할거에요 -> 머티리얼이 있을 때 처리 + _material.SetColor(EmissionProperty, Color.black); // 값을 설정할거에요 -> 발광을 검정(=0)으로 만들어 끄기 + this.enabled = false; // 컴포넌트를 끌거에요 -> Update를 멈추기 + } // 코드 블록을 끝낼거에요 -> 머티리얼 처리 + } // 코드 블록을 끝낼거에요 -> StopHighlight를 + +} // 코드 블록을 끝낼거에요 -> ItemHighlight를 \ No newline at end of file diff --git a/Assets/Scripts/Obsession/BossPatternPhases.cs b/Assets/Scripts/Obsession/BossPatternPhases.cs index 3909c3da..05008c0f 100644 --- a/Assets/Scripts/Obsession/BossPatternPhases.cs +++ b/Assets/Scripts/Obsession/BossPatternPhases.cs @@ -1,123 +1,125 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class BossPatternPhases : MonoBehaviour -{ - // [Header]는 유니티 Inspector 창에서 폴더처럼 보이게 해줌 - [Header("🔧 패턴 설정")] - [SerializeField] private int totalPhases = 3; // 전체 패턴 구간 수 (Inspector에서 조절 가능) +public class BossPatternPhases : MonoBehaviour // 클래스를 선언할거에요 -> 보스 패턴 페이즈 진행을 관리하는 BossPatternPhases를 +{ // 코드 블록을 시작할거에요 -> BossPatternPhases 범위를 - [Header("💎 XP 설정")] - [SerializeField] private int phaseClearXP = 10; // 한 구간 통과 시 주는 XP - [SerializeField] private int fullClearBonus = 30; // 모든 구간 통과 시 보너스 XP - [SerializeField] private int hitPenalty = -5; // 피격 시 감점 XP (0으로 하면 감점 없음) + // [Header]는 유니티 Inspector 창에서 폴더처럼 보이게 해줌 // 설명을 적을거에요 -> 인스펙터 그룹 라벨용 + [Header("🔧 패턴 설정")] // 인스펙터에 제목을 표시할거에요 -> 패턴 설정을 + [SerializeField] private int totalPhases = 3; // 변수를 선언할거에요 -> 전체 페이즈 수를 3으로 - // [SerializeField]는 인스펙터에서 실시간으로 볼 수 있게 해줌 - [Header("🚩 상태 확인용")] - [SerializeField] private int currentPhase = 0; // 현재 진행 중인 구간 (0: 시작 전) - [SerializeField] private bool isPatternActive = false; // 패턴이 진행 중인지 - [SerializeField] private bool wasHitThisPhase = false; // 이번 구간 중 피격당했는지 + [Header("💎 XP 설정")] // 인스펙터에 제목을 표시할거에요 -> XP 설정을 + [SerializeField] private int phaseClearXP = 10; // 변수를 선언할거에요 -> 페이즈 성공 시 XP를 10으로 + [SerializeField] private int fullClearBonus = 30; // 변수를 선언할거에요 -> 전체 클리어 보너스를 30으로 + [SerializeField] private int hitPenalty = -5; // 변수를 선언할거에요 -> 피격 시 감점 XP를 -5로 - // ★ 초기화 - private void Start() - { - ResetPattern(); - } + // [SerializeField]는 인스펙터에서 실시간으로 볼 수 있게 해줌 // 설명을 적을거에요 -> 런타임에도 값 확인 가능 + [Header("🚩 상태 확인용")] // 인스펙터에 제목을 표시할거에요 -> 상태 확인용을 + [SerializeField] private int currentPhase = 0; // 변수를 선언할거에요 -> 현재 페이즈를 0으로(시작 전) + [SerializeField] private bool isPatternActive = false; // 변수를 선언할거에요 -> 패턴 진행 여부를 false로 + [SerializeField] private bool wasHitThisPhase = false; // 변수를 선언할거에요 -> 이번 페이즈 피격 여부를 false로 - // ★ 패턴 전체 리셋 (새로운 패턴 시작 시 호출) - public void ResetPattern() - { - currentPhase = 0; - wasHitThisPhase = false; - isPatternActive = false; - Debug.Log("[보스] 패턴 초기화 완료!"); - } + // ★ 초기화 // 설명을 적을거에요 -> 시작 시 초기화 수행 + private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를 + { // 코드 블록을 시작할거에요 -> Start 범위를 + ResetPattern(); // 함수를 실행할거에요 -> 패턴 상태를 초기화 + } // 코드 블록을 끝낼거에요 -> Start를 - // ★ 패턴 시작 (보스 AI에서 호출) - public void StartPattern() - { - if (currentPhase >= totalPhases) - { - Debug.Log("이미 모든 패턴을 완료했어요!"); - return; - } + // ★ 패턴 전체 리셋 (새로운 패턴 시작 시 호출) // 설명을 적을거에요 -> 상태를 처음으로 되돌리는 함수 + public void ResetPattern() // 함수를 선언할거에요 -> 패턴 전체 초기화 ResetPattern을 + { // 코드 블록을 시작할거에요 -> ResetPattern 범위를 + currentPhase = 0; // 값을 설정할거에요 -> 현재 페이즈를 0으로 + wasHitThisPhase = false; // 값을 설정할거에요 -> 피격 여부를 false로 + isPatternActive = false; // 값을 설정할거에요 -> 패턴 진행 상태를 false로 + Debug.Log("[보스] 패턴 초기화 완료!"); // 로그를 찍을거에요 -> 초기화 완료 확인 + } // 코드 블록을 끝낼거에요 -> ResetPattern을 - isPatternActive = true; - wasHitThisPhase = false; - Debug.Log($"🔔 [페이즈 {currentPhase + 1}] 시작! 회피하세요!"); + // ★ 패턴 시작 (보스 AI에서 호출) // 설명을 적을거에요 -> 보스가 패턴을 시작할 때 호출 + public void StartPattern() // 함수를 선언할거에요 -> 패턴 시작 함수 StartPattern을 + { // 코드 블록을 시작할거에요 -> StartPattern 범위를 + if (currentPhase >= totalPhases) // 조건을 검사할거에요 -> 이미 전부 끝났는지 + { // 코드 블록을 시작할거에요 -> 전부 끝난 경우 처리 + Debug.Log("이미 모든 패턴을 완료했어요!"); // 로그를 찍을거에요 -> 더 이상 시작 불가 안내 + return; // 함수를 끝낼거에요 -> 아래 로직 실행 안 하게 + } // 코드 블록을 끝낼거에요 -> 완료 처리 - // ★ ★ ★ 여기서 보스의 애니메이션/이펙트 시작하면 됨 ★ ★ ★ - } + isPatternActive = true; // 값을 설정할거에요 -> 패턴 진행 상태를 true로 + wasHitThisPhase = false; // 값을 설정할거에요 -> 이번 페이즈 피격 여부 초기화 + Debug.Log($"🔔 [페이즈 {currentPhase + 1}] 시작! 회피하세요!"); // 로그를 찍을거에요 -> 페이즈 시작 알림 - // ★ 플레이어가 피격당했을 때 호출 (PlayerHealth에서 연결) - public void OnPlayerHit() - { - if (!isPatternActive) return; + // ★ ★ ★ 여기서 보스의 애니메이션/이펙트 시작하면 됨 ★ ★ ★ // 설명을 적을거에요 -> 연출 시작 지점 + } // 코드 블록을 끝낼거에요 -> StartPattern을 - wasHitThisPhase = true; - Debug.Log($"💥 [페이즈 {currentPhase + 1}] 중 피격당함! (XP 감점)"); + // ★ 플레이어가 피격당했을 때 호출 (PlayerHealth에서 연결) // 설명을 적을거에요 -> 플레이어 피격 이벤트 연결용 + public void OnPlayerHit() // 함수를 선언할거에요 -> 피격 처리 함수 OnPlayerHit을 + { // 코드 블록을 시작할거에요 -> OnPlayerHit 범위를 + if (!isPatternActive) return; // 조건이 맞으면 종료할거에요 -> 패턴 중이 아니면 무시 - // 피격 시 감점 처리 (필요하면) - if (hitPenalty != 0 && ObsessionSystem.instance != null) - { - ObsessionSystem.instance.AddRunXP(hitPenalty); - } - } + wasHitThisPhase = true; // 값을 설정할거에요 -> 이번 페이즈에서 맞았다고 표시 + Debug.Log($"💥 [페이즈 {currentPhase + 1}] 중 피격당함! (XP 감점)"); // 로그를 찍을거에요 -> 피격 안내 - // ★ 현재 구간을 성공적으로 통과했을 때 호출 (보스 AI에서 호출) - public void ClearCurrentPhase() - { - if (!isPatternActive) return; + // 피격 시 감점 처리 (필요하면) // 설명을 적을거에요 -> hitPenalty가 0이 아니면 XP 감소 + if (hitPenalty != 0 && ObsessionSystem.instance != null) // 조건을 검사할거에요 -> 감점이 있고 시스템이 존재하는지 + { // 코드 블록을 시작할거에요 -> 감점 처리 + ObsessionSystem.instance.AddRunXP(hitPenalty); // XP를 더할거에요 -> 음수면 감점됨 + } // 코드 블록을 끝낼거에요 -> 감점 처리 + } // 코드 블록을 끝낼거에요 -> OnPlayerHit을 - // 피격당했는지 확인 - if (wasHitThisPhase) - { - Debug.Log($"❌ [페이즈 {currentPhase + 1}] 실패... 다음 구간 진행"); - } - else - { - // ★ 성공 시 XP 지급 - if (ObsessionSystem.instance != null) - { - ObsessionSystem.instance.AddRunXP(phaseClearXP); - Debug.Log($"✅ [페이즈 {currentPhase + 1}] 성공! +{phaseClearXP}XP"); - } - } + // ★ 현재 구간을 성공적으로 통과했을 때 호출 (보스 AI에서 호출) // 설명을 적을거에요 -> 페이즈 종료/판정 처리 + public void ClearCurrentPhase() // 함수를 선언할거에요 -> 현재 페이즈 클리어 처리 ClearCurrentPhase를 + { // 코드 블록을 시작할거에요 -> ClearCurrentPhase 범위를 + if (!isPatternActive) return; // 조건이 맞으면 종료할거에요 -> 패턴 중이 아니면 무시 - // 다음 구간으로 - currentPhase++; - wasHitThisPhase = false; + // 피격당했는지 확인 // 설명을 적을거에요 -> 성공/실패 판정 기준 + if (wasHitThisPhase) // 조건을 검사할거에요 -> 이번 페이즈 중 피격 여부 + { // 코드 블록을 시작할거에요 -> 실패 처리 + Debug.Log($"❌ [페이즈 {currentPhase + 1}] 실패... 다음 구간 진행"); // 로그를 찍을거에요 -> 실패 안내 + } // 코드 블록을 끝낼거에요 -> 실패 처리 + else // 조건이 아니면 실행할거에요 -> 안 맞았으면 + { // 코드 블록을 시작할거에요 -> 성공 처리 + // ★ 성공 시 XP 지급 // 설명을 적을거에요 -> 시스템이 있으면 XP 지급 + if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> XP 시스템이 존재하는지 + { // 코드 블록을 시작할거에요 -> XP 지급 + ObsessionSystem.instance.AddRunXP(phaseClearXP); // XP를 더할거에요 -> phaseClearXP만큼 + Debug.Log($"✅ [페이즈 {currentPhase + 1}] 성공! +{phaseClearXP}XP"); // 로그를 찍을거에요 -> 성공 안내 + } // 코드 블록을 끝낼거에요 -> XP 지급 + } // 코드 블록을 끝낼거에요 -> 성공/실패 분기 - // 모든 구간을 완료했는가? - if (currentPhase >= totalPhases) - { - Debug.Log("🎉 모든 패턴 완료!"); - FullClearBonus(); - isPatternActive = false; - } - } + // 다음 구간으로 // 설명을 적을거에요 -> 페이즈 진행 업데이트 + currentPhase++; // 값을 증가시킬거에요 -> 다음 페이즈로 이동 + wasHitThisPhase = false; // 값을 초기화할거에요 -> 다음 페이즈를 위해 피격 여부 리셋 - // ★ 모든 구간 통과 시 보너스 - private void FullClearBonus() - { - if (ObsessionSystem.instance != null) - { - ObsessionSystem.instance.AddRunXP(fullClearBonus); - Debug.Log($"🌟 전체 패턴 클리어 보너스 +{fullClearBonus}XP!"); - } - } + // 모든 구간을 완료했는가? // 설명을 적을거에요 -> 전체 클리어 체크 + if (currentPhase >= totalPhases) // 조건을 검사할거에요 -> 마지막 페이즈까지 끝났는지 + { // 코드 블록을 시작할거에요 -> 전체 클리어 처리 + Debug.Log("🎉 모든 패턴 완료!"); // 로그를 찍을거에요 -> 전체 완료 안내 + FullClearBonus(); // 함수를 실행할거에요 -> 보너스 XP 지급 + isPatternActive = false; // 값을 설정할거에요 -> 패턴 종료 상태로 + } // 코드 블록을 끝낼거에요 -> 전체 클리어 처리 + } // 코드 블록을 끝낼거에요 -> ClearCurrentPhase를 - // ★ 디버그용: 현재 상태를 콘솔에 출력 - private void Update() - { - if (Input.GetKeyDown(KeyCode.P)) - { - Debug.Log($"[패턴 상태] 현재 구간: {currentPhase + 1}/{totalPhases}, 활성: {isPatternActive}"); - } - } + // ★ 모든 구간 통과 시 보너스 // 설명을 적을거에요 -> 전체 클리어 보너스 지급 + private void FullClearBonus() // 함수를 선언할거에요 -> 전체 클리어 보너스 FullClearBonus를 + { // 코드 블록을 시작할거에요 -> FullClearBonus 범위를 + if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> XP 시스템이 존재하는지 + { // 코드 블록을 시작할거에요 -> 보너스 지급 + ObsessionSystem.instance.AddRunXP(fullClearBonus); // XP를 더할거에요 -> 보너스 XP 지급 + Debug.Log($"🌟 전체 패턴 클리어 보너스 +{fullClearBonus}XP!"); // 로그를 찍을거에요 -> 보너스 안내 + } // 코드 블록을 끝낼거에요 -> 보너스 지급 + } // 코드 블록을 끝낼거에요 -> FullClearBonus를 - // BossPatternPhases.cs에 추가 - public bool IsPatternActive() - { - return isPatternActive; - } -} \ No newline at end of file + // ★ 디버그용: 현재 상태를 콘솔에 출력 // 설명을 적을거에요 -> 테스트용 단축키 출력 + private void Update() // 함수를 선언할거에요 -> 매 프레임 호출되는 Update를 + { // 코드 블록을 시작할거에요 -> Update 범위를 + if (Input.GetKeyDown(KeyCode.P)) // 조건을 검사할거에요 -> P 키를 눌렀는지 + { // 코드 블록을 시작할거에요 -> 디버그 출력 + Debug.Log($"[패턴 상태] 현재 구간: {currentPhase + 1}/{totalPhases}, 활성: {isPatternActive}"); // 로그를 찍을거에요 -> 현재 상태를 + } // 코드 블록을 끝낼거에요 -> 디버그 출력 + } // 코드 블록을 끝낼거에요 -> Update를 + + // BossPatternPhases.cs에 추가 // 설명을 적을거에요 -> 외부에서 패턴 진행 여부 확인용 + public bool IsPatternActive() // 함수를 선언할거에요 -> 패턴 활성 여부 반환 함수 IsPatternActive를 + { // 코드 블록을 시작할거에요 -> IsPatternActive 범위를 + return isPatternActive; // 값을 돌려줄거에요 -> 현재 활성 상태를 + } // 코드 블록을 끝낼거에요 -> IsPatternActive를 + +} // 코드 블록을 끝낼거에요 -> BossPatternPhases를 \ No newline at end of file diff --git a/Assets/Scripts/Obsession/BossRoomTrigger.cs b/Assets/Scripts/Obsession/BossRoomTrigger.cs index 79c81aaa..235db622 100644 --- a/Assets/Scripts/Obsession/BossRoomTrigger.cs +++ b/Assets/Scripts/Obsession/BossRoomTrigger.cs @@ -1,19 +1,22 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class BossRoomTrigger : MonoBehaviour -{ - private void OnTriggerEnter(Collider other) - { - if (other.CompareTag("Player")) - { - // ★ 보스 방에 처음 들어가면 보너스 XP - if (ObsessionSystem.instance != null) - { - ObsessionSystem.instance.AddRunXP(50); // 보스 도달 보너스 - Debug.Log("보스 방 도달! +50XP 보너스 획득!"); - } +public class BossRoomTrigger : MonoBehaviour // 클래스를 선언할거에요 -> 보스방 진입 트리거를 처리하는 BossRoomTrigger를 +{ // 코드 블록을 시작할거에요 -> BossRoomTrigger 범위를 - Destroy(this); // 한 번만 주고 제거 - } - } -} \ No newline at end of file + private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 뭔가 들어오면 호출되는 OnTriggerEnter를 + { // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를 + if (other.CompareTag("Player")) // 조건을 검사할거에요 -> 들어온 대상이 Player인지 + { // 코드 블록을 시작할거에요 -> 플레이어일 때 처리 + + // ★ 보스 방에 처음 들어가면 보너스 XP // 설명을 적을거에요 -> 1회만 보너스 지급 목적 + if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> XP 시스템 싱글톤이 존재하는지 + { // 코드 블록을 시작할거에요 -> XP 지급 처리 + ObsessionSystem.instance.AddRunXP(50); // XP를 더할거에요 -> 보스 도달 보너스 50을 + Debug.Log("보스 방 도달! +50XP 보너스 획득!"); // 로그를 찍을거에요 -> 보너스 지급 확인 + } // 코드 블록을 끝낼거에요 -> XP 지급 처리 + + Destroy(this); // 컴포넌트를 삭제할거에요 -> 한 번만 발동하게 BossRoomTrigger를 + } // 코드 블록을 끝낼거에요 -> 플레이어 처리 + } // 코드 블록을 끝낼거에요 -> OnTriggerEnter를 + +} // 코드 블록을 끝낼거에요 -> BossRoomTrigger를 \ No newline at end of file diff --git a/Assets/Scripts/Obsession/ObsessionSystem.cs b/Assets/Scripts/Obsession/ObsessionSystem.cs index 6afe7c67..c83a7070 100644 --- a/Assets/Scripts/Obsession/ObsessionSystem.cs +++ b/Assets/Scripts/Obsession/ObsessionSystem.cs @@ -1,118 +1,110 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class ObsessionSystem : MonoBehaviour -{ - public static ObsessionSystem instance; +public class ObsessionSystem : MonoBehaviour // 클래스를 선언할거에요 -> 영구 XP/런 XP/레벨을 관리하는 ObsessionSystem을 +{ // 코드 블록을 시작할거에요 -> ObsessionSystem 범위를 - [SerializeField] private int currentXP = 0; // ★ 영구 XP (저장되는 진짜 진행도) - [SerializeField] private int currentLevel = 1; - [SerializeField] private int[] xpRequirements = { 100, 300, 500, 800, 1200 }; + public static ObsessionSystem instance; // 정적 변수를 선언할거에요 -> 어디서든 접근 가능한 싱글톤 instance를 - // ★ ★ ★ NEW: 이번 런에서 쌓은 임시 XP ★ ★ ★ - [SerializeField] private int currentRunXP = 0; // 이 값은 게임 껐다 키면 사라져요! + [SerializeField] private int currentXP = 0; // 변수를 선언할거에요 -> 영구 XP(저장되는 진행도)를 currentXP에 + [SerializeField] private int currentLevel = 1; // 변수를 선언할거에요 -> 현재 레벨을 currentLevel에 + [SerializeField] private int[] xpRequirements = { 100, 300, 500, 800, 1200 }; // 배열을 선언할거에요 -> 레벨업 필요 XP 목록을 - private void Awake() - { - if (instance == null) - { - instance = this; - DontDestroyOnLoad(gameObject); - } - else - { - Destroy(gameObject); - } - } + [SerializeField] private int currentRunXP = 0; // 변수를 선언할거에요 -> 이번 런에서 쌓인 임시 XP를 currentRunXP에 - private void Start() - { - LoadData(); - Debug.Log($"[집착 시스템] 시작! 현재 레벨: {currentLevel}, XP: {currentXP}"); + private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + if (instance == null) // 조건을 검사할거에요 -> 싱글톤이 아직 없으면 + { // 코드 블록을 시작할거에요 -> 최초 생성 처리 + instance = this; // 값을 넣을거에요 -> instance에 자신을 등록 + DontDestroyOnLoad(gameObject); // 파괴하지 않을거에요 -> 씬이 바뀌어도 유지되게 + } // 코드 블록을 끝낼거에요 -> 최초 생성 처리 + else // 조건이 아니면 실행할거에요 -> 이미 instance가 있으면 + { // 코드 블록을 시작할거에요 -> 중복 제거 처리 + Destroy(gameObject); // 파괴할거에요 -> 중복 오브젝트를 + } // 코드 블록을 끝낼거에요 -> 중복 제거 처리 + } // 코드 블록을 끝낼거에요 -> Awake를 - // ★ ★ ★ NEW: 새 런 시작할 때 임시 XP는 0으로 초기화 ★ ★ ★ - currentRunXP = 0; - Debug.Log($"[새 런 시작] 이번 런에 쌓을 임시 XP: 0"); - } + private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를 + { // 코드 블록을 시작할거에요 -> Start 범위를 + LoadData(); // 함수를 실행할거에요 -> 저장된 데이터 불러오기 + Debug.Log($"[집착 시스템] 시작! 현재 레벨: {currentLevel}, XP: {currentXP}"); // 로그를 찍을거에요 -> 시작 상태 출력 - // ★ ★ ★ NEW: 플레이 중 실시간으로 임시 XP 추가 ★ ★ ★ - public void AddRunXP(int amount) - { - currentRunXP += amount; - Debug.Log($"[런 XP 획득] +{amount}XP (이번 런 총: {currentRunXP})"); - } + currentRunXP = 0; // 값을 초기화할거에요 -> 새 런 시작이니 임시 XP를 0으로 + Debug.Log($"[새 런 시작] 이번 런에 쌓을 임시 XP: 0"); // 로그를 찍을거에요 -> 런 XP 초기화 확인 + } // 코드 블록을 끝낼거에요 -> Start를 - // ★ ★ ★ NEW: 사망 시 임시 XP를 영구 XP로 변환 ★ ★ ★ - public void OnDeathConvertXP() - { - Debug.Log($"[사망] 이번 런 성과: {currentRunXP}XP를 영구 XP로 전환!"); + public void AddRunXP(int amount) // 함수를 선언할거에요 -> 플레이 중 임시 XP를 더하는 AddRunXP를 + { // 코드 블록을 시작할거에요 -> AddRunXP 범위를 + currentRunXP += amount; // 값을 더할거에요 -> 임시 XP에 amount만큼 + Debug.Log($"[런 XP 획득] +{amount}XP (이번 런 총: {currentRunXP})"); // 로그를 찍을거에요 -> 현재 런 XP 출력 + } // 코드 블록을 끝낼거에요 -> AddRunXP를 - // 임시 XP를 영구 XP에 추가 - AddXP(currentRunXP); + public void OnDeathConvertXP() // 함수를 선언할거에요 -> 사망 시 임시 XP를 영구 XP로 바꾸는 OnDeathConvertXP를 + { // 코드 블록을 시작할거에요 -> OnDeathConvertXP 범위를 + Debug.Log($"[사망] 이번 런 성과: {currentRunXP}XP를 영구 XP로 전환!"); // 로그를 찍을거에요 -> 전환 안내 - // 임시 XP는 초기화 (다음 런을 위해) - currentRunXP = 0; - } + AddXP(currentRunXP); // 함수를 실행할거에요 -> 임시 XP만큼 영구 XP에 추가(레벨업/저장 포함) - // 기존 AddXP 함수 (이제는 사망할 때만 호출될 거예요) - public void AddXP(int amount) - { - currentXP += amount; - Debug.Log($"[영구 XP 획득] +{amount}XP (현재: {currentXP})"); - CheckLevelUp(); - SaveData(); - } + currentRunXP = 0; // 값을 초기화할거에요 -> 다음 런을 위해 임시 XP를 0으로 + } // 코드 블록을 끝낼거에요 -> OnDeathConvertXP를 - private void CheckLevelUp() - { - if (currentLevel - 1 < xpRequirements.Length) - { - int requiredXP = xpRequirements[currentLevel - 1]; - if (currentXP >= requiredXP) - { - currentLevel++; - Debug.Log($"🎉 [레벨업!] 레벨 {currentLevel - 1} → {currentLevel}"); - UnlockRewards(currentLevel); - } - } - } + public void AddXP(int amount) // 함수를 선언할거에요 -> 영구 XP를 더하는 AddXP를 + { // 코드 블록을 시작할거에요 -> AddXP 범위를 + currentXP += amount; // 값을 더할거에요 -> 영구 XP에 amount만큼 + Debug.Log($"[영구 XP 획득] +{amount}XP (현재: {currentXP})"); // 로그를 찍을거에요 -> 영구 XP 현황 출력 + CheckLevelUp(); // 함수를 실행할거에요 -> 레벨업 조건 확인 + SaveData(); // 함수를 실행할거에요 -> 변경된 데이터 저장 + } // 코드 블록을 끝낼거에요 -> AddXP를 - private void UnlockRewards(int level) - { - // 보상 코드는 기존과 동일 - } + private void CheckLevelUp() // 함수를 선언할거에요 -> 레벨업 여부를 검사하는 CheckLevelUp을 + { // 코드 블록을 시작할거에요 -> CheckLevelUp 범위를 + if (currentLevel - 1 < xpRequirements.Length) // 조건을 검사할거에요 -> 요구치 배열 범위 안인지 + { // 코드 블록을 시작할거에요 -> 범위 안이면 + int requiredXP = xpRequirements[currentLevel - 1]; // 값을 가져올거에요 -> 현재 레벨의 필요 XP를 requiredXP에 + if (currentXP >= requiredXP) // 조건을 검사할거에요 -> 영구 XP가 요구치 이상인지 + { // 코드 블록을 시작할거에요 -> 레벨업 처리 + currentLevel++; // 레벨을 올릴거에요 -> 현재 레벨 +1 + Debug.Log($"🎉 [레벨업!] 레벨 {currentLevel - 1} → {currentLevel}"); // 로그를 찍을거에요 -> 레벨업 안내 + UnlockRewards(currentLevel); // 함수를 실행할거에요 -> 레벨 보상 해금 처리 + } // 코드 블록을 끝낼거에요 -> 레벨업 처리 + } // 코드 블록을 끝낼거에요 -> 범위 체크 + } // 코드 블록을 끝낼거에요 -> CheckLevelUp을 - private void SaveData() - { - PlayerPrefs.SetInt("ObsessionLevel", currentLevel); - PlayerPrefs.SetInt("ObsessionXP", currentXP); - PlayerPrefs.Save(); - } + private void UnlockRewards(int level) // 함수를 선언할거에요 -> 레벨 보상을 해금하는 UnlockRewards를 + { // 코드 블록을 시작할거에요 -> UnlockRewards 범위를 + // 보상 코드는 기존과 동일 // 설명을 적을거에요 -> 보상 구현은 여기에 추가하면 됨 + } // 코드 블록을 끝낼거에요 -> UnlockRewards를 - private void LoadData() - { - currentLevel = PlayerPrefs.GetInt("ObsessionLevel", 1); - currentXP = PlayerPrefs.GetInt("ObsessionXP", 0); - } + private void SaveData() // 함수를 선언할거에요 -> PlayerPrefs에 저장하는 SaveData를 + { // 코드 블록을 시작할거에요 -> SaveData 범위를 + PlayerPrefs.SetInt("ObsessionLevel", currentLevel); // 값을 저장할거에요 -> 레벨을 ObsessionLevel 키로 + PlayerPrefs.SetInt("ObsessionXP", currentXP); // 값을 저장할거에요 -> 영구 XP를 ObsessionXP 키로 + PlayerPrefs.Save(); // 저장을 확정할거에요 -> PlayerPrefs.Save 호출 + } // 코드 블록을 끝낼거에요 -> SaveData를 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // 🧪 테스트용: 현재 상태를 읽는 함수들 - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + private void LoadData() // 함수를 선언할거에요 -> PlayerPrefs에서 불러오는 LoadData를 + { // 코드 블록을 시작할거에요 -> LoadData 범위를 + currentLevel = PlayerPrefs.GetInt("ObsessionLevel", 1); // 값을 불러올거에요 -> 저장된 레벨이 없으면 1로 + currentXP = PlayerPrefs.GetInt("ObsessionXP", 0); // 값을 불러올거에요 -> 저장된 XP가 없으면 0으로 + } // 코드 블록을 끝낼거에요 -> LoadData를 - // 현재 런 XP 확인 - public int GetCurrentRunXP() - { - return currentRunXP; - } + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 구분선을 적을거에요 -> 테스트/조회 섹션 시작을 + // 🧪 테스트용: 현재 상태를 읽는 함수들 // 설명을 적을거에요 -> 외부 조회용 getter들 + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 구분선을 적을거에요 -> 테스트/조회 섹션을 - // 현재 영구 XP 확인 - public int GetCurrentXP() - { - return currentXP; - } + public int GetCurrentRunXP() // 함수를 선언할거에요 -> 현재 런 XP를 반환하는 GetCurrentRunXP를 + { // 코드 블록을 시작할거에요 -> GetCurrentRunXP 범위를 + return currentRunXP; // 값을 돌려줄거에요 -> 임시 XP를 + } // 코드 블록을 끝낼거에요 -> GetCurrentRunXP를 - // 현재 레벨 확인 - public int GetCurrentLevel() - { - return currentLevel; - } -} \ No newline at end of file + public int GetCurrentXP() // 함수를 선언할거에요 -> 현재 영구 XP를 반환하는 GetCurrentXP를 + { // 코드 블록을 시작할거에요 -> GetCurrentXP 범위를 + return currentXP; // 값을 돌려줄거에요 -> 영구 XP를 + } // 코드 블록을 끝낼거에요 -> GetCurrentXP를 + + public int GetCurrentLevel() // 함수를 선언할거에요 -> 현재 레벨을 반환하는 GetCurrentLevel을 + { // 코드 블록을 시작할거에요 -> GetCurrentLevel 범위를 + return currentLevel; // 값을 돌려줄거에요 -> 현재 레벨을 + } // 코드 블록을 끝낼거에요 -> GetCurrentLevel을 + +} // 코드 블록을 끝낼거에요 -> ObsessionSystem을 \ No newline at end of file diff --git a/Assets/Scripts/Obsession/RoomClear.cs b/Assets/Scripts/Obsession/RoomClear.cs index 9b6ba5c8..561600cc 100644 --- a/Assets/Scripts/Obsession/RoomClear.cs +++ b/Assets/Scripts/Obsession/RoomClear.cs @@ -1,14 +1,18 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class RoomClear : MonoBehaviour -{ - public void ClearRoom() - { - // ★ 방을 무사히 클리어하면 - if (ObsessionSystem.instance != null) - { - ObsessionSystem.instance.AddRunXP(20); - Debug.Log("방 클리어! +20XP"); - } - } -} \ No newline at end of file +public class RoomClear : MonoBehaviour // 클래스를 선언할거에요 -> 방 클리어 보상을 주는 RoomClear를 +{ // 코드 블록을 시작할거에요 -> RoomClear 범위를 + + public void ClearRoom() // 함수를 선언할거에요 -> 방을 클리어했을 때 호출할 ClearRoom을 + { // 코드 블록을 시작할거에요 -> ClearRoom 범위를 + + // ★ 방을 무사히 클리어하면 // 설명을 적을거에요 -> 클리어 보상 지급 구간 + if (ObsessionSystem.instance != null) // 조건을 검사할거에요 -> 집착 시스템이 존재하는지 + { // 코드 블록을 시작할거에요 -> XP 지급 처리 + ObsessionSystem.instance.AddRunXP(20); // 함수를 실행할거에요 -> 이번 런 XP에 20 추가 + Debug.Log("방 클리어! +20XP"); // 로그를 찍을거에요 -> 보상 지급 확인 + } // 코드 블록을 끝낼거에요 -> XP 지급 처리 + + } // 코드 블록을 끝낼거에요 -> ClearRoom을 + +} // 코드 블록을 끝낼거에요 -> RoomClear를 \ No newline at end of file diff --git a/Assets/Scripts/Player/Animation.cs b/Assets/Scripts/Player/Animation.cs index 5e9d57cd..a3a249ec 100644 --- a/Assets/Scripts/Player/Animation.cs +++ b/Assets/Scripts/Player/Animation.cs @@ -1,19 +1,19 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class PlayerAnimator : MonoBehaviour +public class PlayerAnimator : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerAnimator를 { - [SerializeField] private Animator anim; - [SerializeField] private PlayerHealth health; + [SerializeField] private Animator anim; // 변수를 선언할거에요 -> 애니메이터 컴포넌트 anim을 + [SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 플레이어 체력 스크립트 health를 // ⭐ 추가: 진짜 공격 로직이 있는 스크립트 연결 - [SerializeField] private PlayerAttack attack; + [SerializeField] private PlayerAttack attack; // 변수를 선언할거에요 -> 플레이어 공격 스크립트 attack을 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를 { - if (health != null) + if (health != null) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 { - health.OnHitEvent += () => anim.SetTrigger("Hit"); - health.OnDead += () => anim.SetBool("Die", true); + health.OnHitEvent += () => anim.SetTrigger("Hit"); // 이벤트를 구독할거에요 -> 피격 시 Hit 트리거 발동을 + health.OnDead += () => anim.SetBool("Die", true); // 이벤트를 구독할거에요 -> 사망 시 Die 상태 변경을 } } @@ -21,19 +21,19 @@ public class PlayerAnimator : MonoBehaviour // ⭐ [애니메이션 이벤트 전달자] // 애니메이션 창에 적은 이름과 똑같은 함수를 만듭니다. // --------------------------------------------------------- - public void StartWeaponCollision() + public void StartWeaponCollision() // 함수를 선언할거에요 -> 무기 충돌 시작 이벤트 StartWeaponCollision을 { - if (attack != null) attack.StartWeaponCollision(); + if (attack != null) attack.StartWeaponCollision(); // 실행할거에요 -> 공격 스크립트의 충돌 시작 함수를 } - public void StopWeaponCollision() + public void StopWeaponCollision() // 함수를 선언할거에요 -> 무기 충돌 종료 이벤트 StopWeaponCollision을 { - if (attack != null) attack.StopWeaponCollision(); + if (attack != null) attack.StopWeaponCollision(); // 실행할거에요 -> 공격 스크립트의 충돌 종료 함수를 } // --------------------------------------------------------- - public void UpdateMove(float speedValue) => anim.SetFloat("Speed", speedValue); - // public void TriggerAttack() => anim.SetTrigger("Attack"); - public void TriggerThrow() => anim.SetTrigger("Throw"); - public void SetCharging(bool isCharging) => anim.SetBool("isCharging", isCharging); + public void UpdateMove(float speedValue) => anim.SetFloat("Speed", speedValue); // 값을 전달할거에요 -> 애니메이터의 Speed 파라미터에 + // public void TriggerAttack() => anim.SetTrigger("Attack"); + public void TriggerThrow() => anim.SetTrigger("Throw"); // 신호를 보낼거에요 -> 애니메이터의 Throw 트리거에 + public void SetCharging(bool isCharging) => anim.SetBool("isCharging", isCharging); // 값을 설정할거에요 -> 애니메이터의 isCharging 불리언에 } \ No newline at end of file diff --git a/Assets/Scripts/Player/Combat/Arrow.cs b/Assets/Scripts/Player/Combat/Arrow.cs index 73570cb1..4f7c9b50 100644 --- a/Assets/Scripts/Player/Combat/Arrow.cs +++ b/Assets/Scripts/Player/Combat/Arrow.cs @@ -1,57 +1,57 @@ -using UnityEngine; -using System.Collections; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 /// /// 발사된 화살 발사체 (파티클 프리팹에 부착됨) /// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지) /// [NEW] 속성 데미지(DoT) 시스템 추가 /// -public class PlayerArrow : MonoBehaviour +public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerArrow를 { - [Header("--- 정밀 피격 판정 설정 ---")] - [SerializeField] private float raycastDistance = 0.8f; - [SerializeField] private float raycastRadius = 0.08f; - [SerializeField] private LayerMask hitLayers; - [SerializeField] private Transform arrowTip; + [Header("--- 정밀 피격 판정 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 정밀 피격 판정 설정 --- 을 + [SerializeField] private float raycastDistance = 0.8f; // 변수를 선언할거에요 -> 레이캐스트 거리인 raycastDistance를 + [SerializeField] private float raycastRadius = 0.08f; // 변수를 선언할거에요 -> 레이캐스트 반지름인 raycastRadius를 + [SerializeField] private LayerMask hitLayers; // 변수를 선언할거에요 -> 충돌 레이어인 hitLayers를 + [SerializeField] private Transform arrowTip; // 변수를 선언할거에요 -> 화살 끝부분 위치인 arrowTip을 - [Header("--- 디버그 시각화 ---")] - [SerializeField] private bool showDebugRay = true; + [Header("--- 디버그 시각화 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 디버그 시각화 --- 를 + [SerializeField] private bool showDebugRay = true; // 변수를 선언할거에요 -> 디버그 레이 표시 여부인 showDebugRay를 // 화살 스탯 - private float damage; - private float speed; - private float range; - private Vector3 startPos; - private Vector3 shootDirection; - private bool isFired = false; - private bool hasHit = false; + private float damage; // 변수를 선언할거에요 -> 데미지 damage를 + private float speed; // 변수를 선언할거에요 -> 속도 speed를 + private float range; // 변수를 선언할거에요 -> 사거리 range를 + private Vector3 startPos; // 변수를 선언할거에요 -> 시작 위치 startPos를 + private Vector3 shootDirection; // 변수를 선언할거에요 -> 발사 방향 shootDirection을 + private bool isFired = false; // 변수를 초기화할거에요 -> 발사 여부 isFired를 거짓으로 + private bool hasHit = false; // 변수를 초기화할거에요 -> 충돌 여부 hasHit을 거짓으로 // [NEW] 속성 데미지 시스템 - private ArrowElementType elementType = ArrowElementType.None; - private float elementDamage = 0f; - private float elementDuration = 0f; + private ArrowElementType elementType = ArrowElementType.None; // 변수를 초기화할거에요 -> 속성 타입 elementType을 없음으로 + private float elementDamage = 0f; // 변수를 초기화할거에요 -> 속성 데미지 elementDamage를 0으로 + private float elementDuration = 0f; // 변수를 초기화할거에요 -> 속성 지속 시간 elementDuration을 0으로 // Raycast 추적용 - private Vector3 previousPosition; - private Rigidbody rb; + private Vector3 previousPosition; // 변수를 선언할거에요 -> 이전 위치 previousPosition을 + private Rigidbody rb; // 변수를 선언할거에요 -> 물리 컴포넌트 rb를 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를 { - rb = GetComponent(); + rb = GetComponent(); // 컴포넌트를 가져올거에요 -> 리지드바디를 } - private void Start() + private void Start() // 함수를 실행할거에요 -> 활성화 시 Start를 { - if (arrowTip == null) + if (arrowTip == null) // 조건이 맞으면 실행할거에요 -> 화살 팁이 설정되지 않았다면 { - Transform tip = transform.Find("ArrowTip"); - if (tip == null) tip = transform.Find("Tip"); - arrowTip = tip ?? transform; + Transform tip = transform.Find("ArrowTip"); // 찾을거에요 -> 자식 중 ArrowTip을 + if (tip == null) tip = transform.Find("Tip"); // 못 찾으면 찾을거에요 -> Tip을 + arrowTip = tip ?? transform; // 그래도 없으면 할당할거에요 -> 자기 자신의 위치를 } - if (hitLayers.value == 0) + if (hitLayers.value == 0) // 조건이 맞으면 실행할거에요 -> 충돌 레이어가 설정되지 않았다면 { - hitLayers = LayerMask.GetMask("Enemy", "EnemyHitBox", "Wall", "Ground", "Default"); + hitLayers = LayerMask.GetMask("Enemy", "EnemyHitBox", "Wall", "Ground", "Default"); // 값을 넣을거에요 -> 기본 충돌 레이어들을 } } @@ -66,83 +66,83 @@ public class PlayerArrow : MonoBehaviour Vector3 direction, ArrowElementType element, float elemDmg, - float elemDur) + float elemDur) // 함수를 선언할거에요 -> 화살을 초기화하는 Initialize를 { - this.damage = dmg; - this.speed = arrowSpeed; - this.range = maxRange; - this.shootDirection = direction.normalized; - this.startPos = transform.position; - this.previousPosition = transform.position; - this.isFired = true; + this.damage = dmg; // 값을 저장할거에요 -> 데미지를 + this.speed = arrowSpeed; // 값을 저장할거에요 -> 속도를 + this.range = maxRange; // 값을 저장할거에요 -> 사거리를 + this.shootDirection = direction.normalized; // 값을 저장할거에요 -> 정규화된 발사 방향을 + this.startPos = transform.position; // 값을 저장할거에요 -> 시작 위치를 + this.previousPosition = transform.position; // 값을 저장할거에요 -> 이전 위치를 (레이캐스트용) + this.isFired = true; // 상태를 바꿀거에요 -> 발사됨 상태로 // [NEW] 속성 정보 저장 - this.elementType = element; - this.elementDamage = elemDmg; - this.elementDuration = elemDur; + this.elementType = element; // 값을 저장할거에요 -> 속성 타입을 + this.elementDamage = elemDmg; // 값을 저장할거에요 -> 속성 데미지를 + this.elementDuration = elemDur; // 값을 저장할거에요 -> 속성 지속 시간을 // 발사 방향으로 회전 - if (shootDirection != Vector3.zero) + if (shootDirection != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 유효하다면 { - transform.rotation = Quaternion.LookRotation(shootDirection); + transform.rotation = Quaternion.LookRotation(shootDirection); // 회전시킬거에요 -> 발사 방향을 보도록 } // Rigidbody 설정 - if (rb == null) rb = GetComponent(); + if (rb == null) rb = GetComponent(); // 컴포넌트를 가져올거에요 -> 없다면 리지드바디를 - if (rb != null) + if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면 { - rb.useGravity = false; - rb.isKinematic = false; - rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; - rb.velocity = shootDirection * speed; + rb.useGravity = false; // 설정을 바꿀거에요 -> 중력을 끄기로 + rb.isKinematic = false; // 설정을 바꿀거에요 -> 물리 연산을 켜기로 + rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 설정을 바꿀거에요 -> 연속 충돌 감지로 + rb.velocity = shootDirection * speed; // 값을 넣을거에요 -> 방향과 속도를 곱해 물리 속도로 } - Destroy(gameObject, 5f); + Destroy(gameObject, 5f); // 파괴할거에요 -> 5초 뒤에 안전장치로 } /// /// [하위 호환성] 방향 없는 3-파라미터 버전 /// - public void Initialize(float dmg, float arrowSpeed, float maxRange) + public void Initialize(float dmg, float arrowSpeed, float maxRange) // 함수를 선언할거에요 -> 구버전 호환용 초기화 함수를 { Initialize(dmg, arrowSpeed, maxRange, transform.forward, - ArrowElementType.None, 0f, 0f); + ArrowElementType.None, 0f, 0f); // 실행할거에요 -> 신버전 초기화 함수를 기본값으로 } - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를 { - if (!isFired || hasHit) return; + if (!isFired || hasHit) return; // 조건이 맞으면 중단할거에요 -> 발사되지 않았거나 이미 맞았다면 // 사거리 체크 - float traveledDistance = Vector3.Distance(startPos, transform.position); - if (traveledDistance >= range) + float traveledDistance = Vector3.Distance(startPos, transform.position); // 거리를 계산할거에요 -> 이동 거리를 + if (traveledDistance >= range) // 조건이 맞으면 실행할거에요 -> 사거리를 넘었다면 { - Destroy(gameObject); - return; + Destroy(gameObject); // 파괴할거에요 -> 화살을 + return; // 중단할거에요 -> 함수를 } // 정밀 충돌 감지 - CheckPrecisionCollision(); + CheckPrecisionCollision(); // 함수를 실행할거에요 -> 정밀 충돌 감지 함수를 - previousPosition = transform.position; + previousPosition = transform.position; // 값을 저장할거에요 -> 현재 위치를 이전 위치로 } /// /// SphereCast 기반 정밀 충돌 (기존 로직 100% 유지) /// - private void CheckPrecisionCollision() + private void CheckPrecisionCollision() // 함수를 선언할거에요 -> 정밀 충돌을 체크하는 CheckPrecisionCollision을 { - Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; + Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치를 Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f ? rb.velocity.normalized - : transform.forward; + : transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면을 - float frameDistance = Vector3.Distance(previousPosition, transform.position); - float checkDistance = Mathf.Max(frameDistance, raycastDistance); + float frameDistance = Vector3.Distance(previousPosition, transform.position); // 거리를 계산할거에요 -> 프레임 간 이동 거리를 + float checkDistance = Mathf.Max(frameDistance, raycastDistance); // 값을 결정할거에요 -> 이동 거리와 최소 거리 중 큰 값을 - RaycastHit hit; + RaycastHit hit; // 변수를 선언할거에요 -> 충돌 정보를 담을 hit을 bool didHit = Physics.SphereCast( tipPosition, raycastRadius, @@ -150,64 +150,64 @@ public class PlayerArrow : MonoBehaviour out hit, checkDistance, hitLayers - ); + ); // 실행할거에요 -> 구체 캐스트를 쏴서 충돌 여부를 확인하는 것을 - if (showDebugRay) + if (showDebugRay) // 조건이 맞으면 실행할거에요 -> 디버그 모드라면 { - Debug.DrawRay(tipPosition, direction * checkDistance, didHit ? Color.red : Color.green, 0.1f); + Debug.DrawRay(tipPosition, direction * checkDistance, didHit ? Color.red : Color.green, 0.1f); // 선을 그릴거에요 -> 충돌 시 빨강, 아니면 초록으로 } - if (didHit) + if (didHit) // 조건이 맞으면 실행할거에요 -> 충돌했다면 { - HandleHit(hit.collider, hit.point); + HandleHit(hit.collider, hit.point); // 실행할거에요 -> 충돌 처리 함수 HandleHit을 } } /// /// [MODIFIED] 충돌 처리 — 속성 데미지 적용 추가 /// - private void HandleHit(Collider hitCollider, Vector3 hitPoint) + private void HandleHit(Collider hitCollider, Vector3 hitPoint) // 함수를 선언할거에요 -> 충돌 결과를 처리하는 HandleHit을 { - if (hasHit) return; - hasHit = true; + if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 충돌 처리되었다면 + hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로 // 적 감지 (Tag 또는 Layer) - bool isEnemy = hitCollider.CompareTag("Enemy"); + bool isEnemy = hitCollider.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지 // EnemyHitBox 레이어 체크 (레이어가 존재하는 경우에만) - int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); - if (!isEnemy && enemyHitBoxLayerIndex != -1) + int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 레이어 인덱스를 + if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면 { - isEnemy = hitCollider.gameObject.layer == enemyHitBoxLayerIndex; + isEnemy = hitCollider.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 해당 레이어인지 확인해서 } - if (isEnemy) + if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면 { // MonsterClass를 찾아 데미지 적용 - MonsterClass monster = hitCollider.GetComponentInParent(); - if (monster == null) monster = hitCollider.GetComponent(); + MonsterClass monster = hitCollider.GetComponentInParent(); // 컴포넌트를 찾을거에요 -> 부모의 몬스터 클래스를 + if (monster == null) monster = hitCollider.GetComponent(); // 없으면 찾을거에요 -> 자신의 몬스터 클래스를 - if (monster != null) + if (monster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 있다면 { // 1. 기본 데미지 적용 - monster.TakeDamage(damage); - Debug.Log($"적 명중! 기본데미지: {damage}"); + monster.TakeDamage(damage); // 실행할거에요 -> 데미지 입히기 함수를 + Debug.Log($"적 명중! 기본데미지: {damage}"); // 로그를 출력할거에요 -> 명중 메시지를 // 2. [NEW] 속성 효과 적용 - ApplyElementEffect(monster); + ApplyElementEffect(monster); // 실행할거에요 -> 속성 효과 적용 함수를 } - Destroy(gameObject, 0.05f); + Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 잠시 뒤 } - else if (hitCollider.CompareTag("Wall") || hitCollider.CompareTag("Ground")) + else if (hitCollider.CompareTag("Wall") || hitCollider.CompareTag("Ground")) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면 { - Debug.Log($"벽/바닥 충돌! 위치: {hitPoint}"); - Destroy(gameObject, 0.05f); + Debug.Log($"벽/바닥 충돌! 위치: {hitPoint}"); // 로그를 출력할거에요 -> 충돌 위치를 + Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 } - else + else // 조건이 틀리면 실행할거에요 -> 그 외의 충돌이라면 { // 기타 충돌 (Unknown) - Destroy(gameObject, 0.05f); + Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 } } @@ -215,31 +215,31 @@ public class PlayerArrow : MonoBehaviour /// [NEW] 속성 효과 적용 /// MonsterClass에 ApplyStatusEffect 메서드가 있어야 합니다. /// - private void ApplyElementEffect(MonsterClass monster) + private void ApplyElementEffect(MonsterClass monster) // 함수를 선언할거에요 -> 속성 효과를 적용하는 ApplyElementEffect를 { - if (elementType == ArrowElementType.None || elementDamage <= 0f) return; + if (elementType == ArrowElementType.None || elementDamage <= 0f) return; // 조건이 맞으면 중단할거에요 -> 속성이 없거나 데미지가 없다면 // MonsterClass에 ApplyStatusEffect가 있는지 확인 - switch (elementType) + switch (elementType) // 분기할거에요 -> 속성 타입에 따라 { - case ArrowElementType.Fire: - monster.ApplyStatusEffect(StatusEffectType.Burn, elementDamage, elementDuration); - Debug.Log($"화염 효과! {elementDamage} 데미지 x {elementDuration}초"); + case ArrowElementType.Fire: // 조건이 맞으면 실행할거에요 -> 불 속성이라면 + monster.ApplyStatusEffect(StatusEffectType.Burn, elementDamage, elementDuration); // 실행할거에요 -> 화상 효과 적용을 + Debug.Log($"화염 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을 break; - case ArrowElementType.Ice: - monster.ApplyStatusEffect(StatusEffectType.Slow, elementDamage, elementDuration); - Debug.Log($"빙결 효과! 슬로우 {elementDuration}초"); + case ArrowElementType.Ice: // 조건이 맞으면 실행할거에요 -> 얼음 속성이라면 + monster.ApplyStatusEffect(StatusEffectType.Slow, elementDamage, elementDuration); // 실행할거에요 -> 슬로우 효과 적용을 + Debug.Log($"빙결 효과! 슬로우 {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을 break; - case ArrowElementType.Poison: - monster.ApplyStatusEffect(StatusEffectType.Poison, elementDamage, elementDuration); - Debug.Log($"독 효과! {elementDamage} 데미지 x {elementDuration}초"); + case ArrowElementType.Poison: // 조건이 맞으면 실행할거에요 -> 독 속성이라면 + monster.ApplyStatusEffect(StatusEffectType.Poison, elementDamage, elementDuration); // 실행할거에요 -> 독 효과 적용을 + Debug.Log($"독 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을 break; - case ArrowElementType.Lightning: - monster.ApplyStatusEffect(StatusEffectType.Shock, elementDamage, elementDuration); - Debug.Log($"감전 효과! {elementDamage} 범위 데미지"); + case ArrowElementType.Lightning: // 조건이 맞으면 실행할거에요 -> 번개 속성이라면 + monster.ApplyStatusEffect(StatusEffectType.Shock, elementDamage, elementDuration); // 실행할거에요 -> 감전(스턴) 효과 적용을 + Debug.Log($"감전 효과! {elementDamage} 범위 데미지"); // 로그를 출력할거에요 -> 효과 설명을 break; } } @@ -247,58 +247,58 @@ public class PlayerArrow : MonoBehaviour /// /// 백업 충돌 감지 (OnTriggerEnter) — 기존 로직 유지 /// - private void OnTriggerEnter(Collider other) + private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거 충돌 시 OnTriggerEnter를 { - if (hasHit) return; + if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 맞았다면 - bool isEnemy = other.CompareTag("Enemy"); - int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); - if (!isEnemy && enemyHitBoxLayerIndex != -1) + bool isEnemy = other.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지 + int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 히트박스 레이어 인덱스를 + if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면 { - isEnemy = other.gameObject.layer == enemyHitBoxLayerIndex; + isEnemy = other.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 레이어 확인으로 } - if (isEnemy) + if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면 { - HandleHit(other, other.ClosestPoint(transform.position)); + HandleHit(other, other.ClosestPoint(transform.position)); // 실행할거에요 -> 충돌 처리 함수를 } - else if (other.CompareTag("Wall") || other.CompareTag("Ground")) + else if (other.CompareTag("Wall") || other.CompareTag("Ground")) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면 { - hasHit = true; - Destroy(gameObject, 0.05f); + hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로 + Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 } } /// /// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가 /// - private void OnDrawGizmos() + private void OnDrawGizmos() // 함수를 실행할거에요 -> 기즈모를 그리는 OnDrawGizmos를 { - if (!Application.isPlaying || !isFired || !showDebugRay) return; + if (!Application.isPlaying || !isFired || !showDebugRay) return; // 조건이 맞으면 중단할거에요 -> 실행 중이 아니거나 발사 전이거나 디버그가 꺼졌다면 - Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; + Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치로 Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f ? rb.velocity.normalized - : transform.forward; + : transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면으로 - Gizmos.color = hasHit ? Color.red : Color.green; - Gizmos.DrawWireSphere(tipPosition, raycastRadius); - Gizmos.DrawWireSphere(tipPosition + direction * raycastDistance, raycastRadius); + Gizmos.color = hasHit ? Color.red : Color.green; // 색상을 설정할거에요 -> 맞았으면 빨강, 아니면 초록으로 + Gizmos.DrawWireSphere(tipPosition, raycastRadius); // 그림을 그릴거에요 -> 팁 위치에 구체를 + Gizmos.DrawWireSphere(tipPosition + direction * raycastDistance, raycastRadius); // 그림을 그릴거에요 -> 예상 도달 위치에 구체를 - Gizmos.color = Color.yellow; - Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance); + Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로 + Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance); // 선을 그릴거에요 -> 궤적을 표시하는 // [NEW] 속성별 색상 표시 - switch (elementType) + switch (elementType) // 분기할거에요 -> 속성 타입에 따라 { - case ArrowElementType.Fire: Gizmos.color = Color.red; break; - case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; - case ArrowElementType.Poison: Gizmos.color = Color.green; break; - case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; + case ArrowElementType.Fire: Gizmos.color = Color.red; break; // 색상을 바꿀거에요 -> 빨강으로 + case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; // 색상을 바꿀거에요 -> 청록으로 + case ArrowElementType.Poison: Gizmos.color = Color.green; break; // 색상을 바꿀거에요 -> 초록으로 + case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; // 색상을 바꿀거에요 -> 노랑으로 } - if (elementType != ArrowElementType.None) + if (elementType != ArrowElementType.None) // 조건이 맞으면 실행할거에요 -> 속성이 있다면 { - Gizmos.DrawWireSphere(transform.position, 0.5f); + Gizmos.DrawWireSphere(transform.position, 0.5f); // 그림을 그릴거에요 -> 속성 표시용 구체를 } } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Combat/ArrowData.cs b/Assets/Scripts/Player/Combat/ArrowData.cs index bb28ceb6..bba9a055 100644 --- a/Assets/Scripts/Player/Combat/ArrowData.cs +++ b/Assets/Scripts/Player/Combat/ArrowData.cs @@ -1,46 +1,18 @@ -// ============================================ -// 📁 파일명: ArrowData.cs -// 📂 경로: Assets/Scripts/Data/ArrowData.cs -// ============================================ +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -using UnityEngine; - -/// -/// 화살 정보를 담는 데이터 구조체 -/// ArrowPickup.GetArrowData() → PlayerAttack.SetCurrentArrow() 로 전달됩니다. -/// -[System.Serializable] -public struct ArrowData +[CreateAssetMenu(fileName = "New Arrow Data", menuName = "Combat/Arrow Data")] // 메뉴를 만들거에요 -> 에셋 메뉴를 +public class ArrowData : ScriptableObject // 클래스를 선언할거에요 -> 데이터 컨테이너인 ArrowData를 { - [Header("--- 기본 정보 ---")] - public string arrowName; // 화살 이름 (UI 표시용) - public ArrowElementType elementType; // 속성 타입 + [Header("기본 정보")] // 인스펙터 창에 제목을 표시할거에요 -> 기본 정보 를 + public string arrowName; // 변수를 선언할거에요 -> 화살 이름을 + public GameObject projectilePrefab; // 변수를 선언할거에요 -> 투사체 프리팹을 + public Sprite icon; // 변수를 선언할거에요 -> 아이콘 이미지를 - [Header("--- 스탯 ---")] - public float baseDamage; // 기본 데미지 - public float elementDamage; // 속성 추가 데미지 - public float elementDuration; // 속성 지속시간 (초) + [Header("데미지 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 데미지 설정 을 + public float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 물리 데미지를 baseDamage에 (이게 빠져서 오류남) - [Header("--- 프리팹 ---")] - public GameObject projectilePrefab; // 발사할 파티클 프리팹 - - /// - /// 기본 화살 데이터 생성 - /// - public static ArrowData Default => new ArrowData - { - arrowName = "기본 화살", - elementType = ArrowElementType.None, - baseDamage = 10f, - elementDamage = 0f, - elementDuration = 0f, - projectilePrefab = null - }; - - public override string ToString() - { - return $"[{arrowName}] 속성={elementType}, " + - $"기본데미지={baseDamage}, 속성데미지={elementDamage}, " + - $"지속시간={elementDuration}s"; - } + [Header("속성 정보")] // 인스펙터 창에 제목을 표시할거에요 -> 속성 정보 를 + public ArrowElementType elementType; // 변수를 선언할거에요 -> 속성 타입을 + public float elementDamage; // 변수를 선언할거에요 -> 속성 추가 데미지를 + public float elementDuration; // 변수를 선언할거에요 -> 속성 지속 시간을 } \ No newline at end of file diff --git a/Assets/Scripts/Player/Combat/Attack.cs b/Assets/Scripts/Player/Combat/Attack.cs index 058b4ac3..e570e4ce 100644 --- a/Assets/Scripts/Player/Combat/Attack.cs +++ b/Assets/Scripts/Player/Combat/Attack.cs @@ -1,102 +1,102 @@ -using UnityEngine; -using System.Collections; -using System.Collections.Generic; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 -public class PlayerAttack : MonoBehaviour +public class PlayerAttack : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerAttack을 { - [Header("--- 활 설정 ---")] - [SerializeField] private Transform firePoint; - [SerializeField] private PlayerAnimator pAnim; + [Header("--- 활 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 활 설정 --- 을 + [SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 화살 발사 위치인 firePoint를 + [SerializeField] private PlayerAnimator pAnim; // 변수를 선언할거에요 -> 플레이어 애니메이터 스크립트인 pAnim을 - [Header("--- 스탯 참조 ---")] - [SerializeField] private Stats playerStats; + [Header("--- 스탯 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 스탯 참조 --- 를 + [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트인 playerStats를 - [Header("--- 일반 공격 (좌클릭) ---")] - [SerializeField] private float normalRange = 15f; - [SerializeField] private float normalSpeed = 20f; - [SerializeField] private float attackCooldown = 0.5f; + [Header("--- 일반 공격 (좌클릭) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 일반 공격 (좌클릭) --- 을 + [SerializeField] private float normalRange = 15f; // 변수를 선언할거에요 -> 일반 공격 사거리인 normalRange를 + [SerializeField] private float normalSpeed = 20f; // 변수를 선언할거에요 -> 일반 화살 속도인 normalSpeed를 + [SerializeField] private float attackCooldown = 0.5f; // 변수를 선언할거에요 -> 공격 쿨타임인 attackCooldown을 - [Header("--- 차징 공격 (우클릭) ---")] - [SerializeField] private float maxChargeTime = 2.0f; + [Header("--- 차징 공격 (우클릭) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 차징 공격 (우클릭) --- 을 + [SerializeField] private float maxChargeTime = 2.0f; // 변수를 선언할거에요 -> 최대 차징 시간인 maxChargeTime을 - [System.Serializable] - public struct ChargeStage + [System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정할 수 있게 구조체를 + public struct ChargeStage // 구조체를 정의할거에요 -> 차징 단계를 정의하는 ChargeStage를 { - public float chargeTime; - public float damageMult; - public float rangeMult; + public float chargeTime; // 변수를 선언할거에요 -> 도달 시간인 chargeTime을 + public float damageMult; // 변수를 선언할거에요 -> 데미지 배율인 damageMult를 + public float rangeMult; // 변수를 선언할거에요 -> 사거리/속도 배율인 rangeMult를 } - [SerializeField] private List chargeStages; + [SerializeField] private List chargeStages; // 리스트를 선언할거에요 -> 차징 단계 목록인 chargeStages를 - [Header("--- 에임 보정 설정 ---")] - [SerializeField] private bool enableAutoAim = true; - [SerializeField] private float autoAimRange = 15f; - [SerializeField] private float autoAimAngle = 45f; - [Range(0f, 1f)] - [SerializeField] private float aimAssistStrength = 0.4f; - [SerializeField] private LayerMask enemyLayer; - [SerializeField] private bool onlyMaxCharge = true; + [Header("--- 에임 보정 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 에임 보정 설정 --- 을 + [SerializeField] private bool enableAutoAim = true; // 변수를 선언할거에요 -> 자동 조준 사용 여부인 enableAutoAim을 + [SerializeField] private float autoAimRange = 15f; // 변수를 선언할거에요 -> 자동 조준 감지 거리인 autoAimRange를 + [SerializeField] private float autoAimAngle = 45f; // 변수를 선언할거에요 -> 자동 조준 감지 각도인 autoAimAngle을 + [Range(0f, 1f)] // 슬라이더를 만들거에요 -> 0부터 1 사이의 값으로 + [SerializeField] private float aimAssistStrength = 0.4f; // 변수를 선언할거에요 -> 보정 강도인 aimAssistStrength를 + [SerializeField] private LayerMask enemyLayer; // 변수를 선언할거에요 -> 적 레이어인 enemyLayer를 + [SerializeField] private bool onlyMaxCharge = true; // 변수를 선언할거에요 -> 풀차징 때만 유도할지 여부인 onlyMaxCharge를 // ============================================ // [NEW] 파티클 기반 화살 시스템 // ============================================ - [Header("--- 파티클 화살 시스템 ---")] - [SerializeField] private GameObject defaultProjectilePrefab; // 기본 발사체 프리팹 + [Header("--- 파티클 화살 시스템 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 파티클 화살 시스템 --- 을 + [SerializeField] private GameObject defaultProjectilePrefab; // 변수를 선언할거에요 -> 기본 발사체 프리팹인 defaultProjectilePrefab을 - private GameObject _currentProjectilePrefab; - private ArrowElementType _currentElementType = ArrowElementType.None; - private float _currentElementDamage = 0f; - private float _currentElementDuration = 0f; + private GameObject _currentProjectilePrefab; // 변수를 선언할거에요 -> 현재 사용할 발사체 프리팹인 _currentProjectilePrefab을 + private ArrowElementType _currentElementType = ArrowElementType.None; // 변수를 초기화할거에요 -> 현재 화살 속성인 _currentElementType을 없음으로 + private float _currentElementDamage = 0f; // 변수를 초기화할거에요 -> 현재 속성 데미지인 _currentElementDamage를 0으로 + private float _currentElementDuration = 0f; // 변수를 초기화할거에요 -> 현재 속성 지속 시간인 _currentElementDuration을 0으로 - private float _lastAttackTime; - private float _chargeTimer; - private bool _isCharging = false; - private bool _isAttacking = false; - private bool _waitForRelease = false; + private float _lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간인 _lastAttackTime을 + private float _chargeTimer; // 변수를 선언할거에요 -> 차징 진행 시간인 _chargeTimer를 + private bool _isCharging = false; // 변수를 초기화할거에요 -> 차징 중 여부인 _isCharging을 거짓으로 + private bool _isAttacking = false; // 변수를 초기화할거에요 -> 공격 동작 중 여부인 _isAttacking을 거짓으로 + private bool _waitForRelease = false; // 변수를 초기화할거에요 -> 발사 대기 상태 여부인 _waitForRelease를 거짓으로 - private float _pendingDamage; - private float _pendingSpeed; - private float _pendingRange; - private Vector3 _pendingShootDirection; + private float _pendingDamage; // 변수를 선언할거에요 -> 발사될 화살의 데미지인 _pendingDamage를 + private float _pendingSpeed; // 변수를 선언할거에요 -> 발사될 화살의 속도인 _pendingSpeed를 + private float _pendingRange; // 변수를 선언할거에요 -> 발사될 화살의 사거리인 _pendingRange를 + private Vector3 _pendingShootDirection; // 변수를 선언할거에요 -> 발사될 방향인 _pendingShootDirection을 - public bool IsCharging => _isCharging; - public bool IsAttacking { get => _isAttacking; set => _isAttacking = value; } - public float ChargeProgress => Mathf.Clamp01(_chargeTimer / maxChargeTime); + public bool IsCharging => _isCharging; // 프로퍼티를 선언할거에요 -> 차징 중인지 여부를 반환하는 IsCharging을 + public bool IsAttacking { get => _isAttacking; set => _isAttacking = value; } // 프로퍼티를 선언할거에요 -> 공격 중인지 여부를 읽고 쓰는 IsAttacking을 + public float ChargeProgress => Mathf.Clamp01(_chargeTimer / maxChargeTime); // 프로퍼티를 선언할거에요 -> 차징 진행률(0~1)을 반환하는 ChargeProgress를 // [NEW] 외부에서 현재 속성 확인용 - public ArrowElementType CurrentElement => _currentElementType; - public GameObject CurrentProjectilePrefab => _currentProjectilePrefab; + public ArrowElementType CurrentElement => _currentElementType; // 프로퍼티를 선언할거에요 -> 현재 화살 속성을 반환하는 CurrentElement를 + public GameObject CurrentProjectilePrefab => _currentProjectilePrefab; // 프로퍼티를 선언할거에요 -> 현재 발사체 프리팹을 반환하는 CurrentProjectilePrefab을 - private void Start() + private void Start() // 함수를 실행할거에요 -> 스크립트 시작 시 Start를 { - if (playerStats == null) playerStats = GetComponentInParent(); + if (playerStats == null) playerStats = GetComponentInParent(); // 조건이 맞으면 가져올거에요 -> 부모의 Stats 컴포넌트를 playerStats에 // 기본 발사체 프리팹 설정 - if (_currentProjectilePrefab == null) - _currentProjectilePrefab = defaultProjectilePrefab; + if (_currentProjectilePrefab == null) // 조건이 맞으면 실행할거에요 -> 현재 설정된 프리팹이 없다면 + _currentProjectilePrefab = defaultProjectilePrefab; // 값을 넣을거에요 -> 기본 프리팹을 - if (chargeStages == null || chargeStages.Count == 0) + if (chargeStages == null || chargeStages.Count == 0) // 조건이 맞으면 실행할거에요 -> 차징 단계 리스트가 비어있다면 { - chargeStages = new List + chargeStages = new List // 리스트를 생성할거에요 -> 기본 3단계 차징 설정을 담은 리스트를 { - new ChargeStage { chargeTime = 0f, damageMult = 1f, rangeMult = 1f }, - new ChargeStage { chargeTime = 1f, damageMult = 1.5f, rangeMult = 1.2f }, - new ChargeStage { chargeTime = 2f, damageMult = 2.5f, rangeMult = 1.5f } + new ChargeStage { chargeTime = 0f, damageMult = 1f, rangeMult = 1f }, // 값을 넣을거에요 -> 0단계 설정을 + new ChargeStage { chargeTime = 1f, damageMult = 1.5f, rangeMult = 1.2f }, // 값을 넣을거에요 -> 1단계 설정을 + new ChargeStage { chargeTime = 2f, damageMult = 2.5f, rangeMult = 1.5f } // 값을 넣을거에요 -> 2단계 설정을 }; } - float calculatedMaxTime = 0f; - foreach (var stage in chargeStages) + float calculatedMaxTime = 0f; // 변수를 초기화할거에요 -> 계산된 최대 시간을 0으로 + foreach (var stage in chargeStages) // 반복할거에요 -> 모든 차징 단계에 대해 { - if (stage.chargeTime > calculatedMaxTime) calculatedMaxTime = stage.chargeTime; + if (stage.chargeTime > calculatedMaxTime) calculatedMaxTime = stage.chargeTime; // 조건이 맞으면 갱신할거에요 -> 더 긴 시간이 있다면 그 값으로 } - maxChargeTime = Mathf.Max(calculatedMaxTime, 0.1f); + maxChargeTime = Mathf.Max(calculatedMaxTime, 0.1f); // 값을 설정할거에요 -> 계산된 최대 시간으로 (최소 0.1 보장) } - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를 { - if (_isCharging) _chargeTimer += Time.deltaTime; + if (_isCharging) _chargeTimer += Time.deltaTime; // 조건이 맞으면 더할거에요 -> 차징 중이라면 타이머에 시간을 } // ============================================ @@ -107,165 +107,165 @@ public class PlayerAttack : MonoBehaviour /// ArrowPickup에서 습득 시 호출합니다. /// 파티클 프리팹 + 속성 정보를 저장합니다. /// - public void SetCurrentArrow(ArrowData data) + public void SetCurrentArrow(ArrowData data) // 함수를 선언할거에요 -> 새로운 화살 데이터를 적용하는 SetCurrentArrow를 { - if (data.projectilePrefab != null) - _currentProjectilePrefab = data.projectilePrefab; - else - _currentProjectilePrefab = defaultProjectilePrefab; + if (data.projectilePrefab != null) // 조건이 맞으면 실행할거에요 -> 데이터에 프리팹이 있다면 + _currentProjectilePrefab = data.projectilePrefab; // 값을 바꿀거에요 -> 현재 발사체 프리팹을 새 것으로 + else // 조건이 틀리면 실행할거에요 -> 데이터에 프리팹이 없다면 + _currentProjectilePrefab = defaultProjectilePrefab; // 값을 바꿀거에요 -> 기본 프리팹으로 - _currentElementType = data.elementType; - _currentElementDamage = data.elementDamage; - _currentElementDuration = data.elementDuration; + _currentElementType = data.elementType; // 값을 저장할거에요 -> 화살 속성 타입을 + _currentElementDamage = data.elementDamage; // 값을 저장할거에요 -> 속성 데미지를 + _currentElementDuration = data.elementDuration; // 값을 저장할거에요 -> 속성 지속 시간을 Debug.Log($"화살 장착: [{data.arrowName}] 속성={data.elementType}, " + - $"속성데미지={data.elementDamage}, 지속시간={data.elementDuration}s"); + $"속성데미지={data.elementDamage}, 지속시간={data.elementDuration}s"); // 로그를 출력할거에요 -> 장착된 화살 정보를 } /// /// 기본 화살로 초기화 /// - public void ResetArrow() + public void ResetArrow() // 함수를 선언할거에요 -> 화살을 기본 상태로 되돌리는 ResetArrow를 { - _currentProjectilePrefab = defaultProjectilePrefab; - _currentElementType = ArrowElementType.None; - _currentElementDamage = 0f; - _currentElementDuration = 0f; - Debug.Log("화살 초기화: 기본 화살"); + _currentProjectilePrefab = defaultProjectilePrefab; // 값을 바꿀거에요 -> 발사체를 기본 프리팹으로 + _currentElementType = ArrowElementType.None; // 값을 바꿀거에요 -> 속성을 없음(None)으로 + _currentElementDamage = 0f; // 값을 초기화할거에요 -> 속성 데미지를 0으로 + _currentElementDuration = 0f; // 값을 초기화할거에요 -> 속성 시간을 0으로 + Debug.Log("화살 초기화: 기본 화살"); // 로그를 출력할거에요 -> 초기화 완료 메시지를 } // ============================================ // 일반 공격 (좌클릭) — 기존 로직 100% 유지 // ============================================ - public void PerformNormalAttack() + public void PerformNormalAttack() // 함수를 선언할거에요 -> 일반 공격을 수행하는 PerformNormalAttack을 { - if (Time.time < _lastAttackTime + attackCooldown || _isAttacking) return; + if (Time.time < _lastAttackTime + attackCooldown || _isAttacking) return; // 조건이 맞으면 중단할거에요 -> 쿨타임 중이거나 이미 공격 중이라면 - _pendingDamage = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; - _pendingSpeed = normalSpeed; - _pendingRange = normalRange; + _pendingDamage = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; // 값을 가져올거에요 -> 스탯이 있으면 총 공격력을, 없으면 10을 + _pendingSpeed = normalSpeed; // 값을 저장할거에요 -> 기본 속도를 + _pendingRange = normalRange; // 값을 저장할거에요 -> 기본 사거리를 - _pendingShootDirection = GetMouseDirection(); - _lastAttackTime = Time.time; + _pendingShootDirection = GetMouseDirection(); // 값을 계산할거에요 -> 마우스 방향을 구해서 발사 방향으로 + _lastAttackTime = Time.time; // 값을 갱신할거에요 -> 마지막 공격 시간을 현재로 - if (pAnim != null) pAnim.TriggerThrow(); - StartCoroutine(AttackRoutine()); + if (pAnim != null) pAnim.TriggerThrow(); // 실행할거에요 -> 던지는 애니메이션 트리거를 + StartCoroutine(AttackRoutine()); // 코루틴을 시작할거에요 -> 공격 상태 관리를 위한 AttackRoutine을 } // ============================================ // 차징 공격 (우클릭) — 기존 로직 100% 유지 // ============================================ - public void StartCharging() + public void StartCharging() // 함수를 선언할거에요 -> 차징을 시작하는 StartCharging을 { - if (_waitForRelease) return; - _isCharging = true; - _chargeTimer = 0f; - if (pAnim != null) pAnim.SetCharging(true); - if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true); + if (_waitForRelease) return; // 조건이 맞으면 중단할거에요 -> 이미 발사 대기 중이라면 + _isCharging = true; // 상태를 바꿀거에요 -> 차징 중 상태를 참(true)으로 + _chargeTimer = 0f; // 값을 초기화할거에요 -> 차징 타이머를 0으로 + if (pAnim != null) pAnim.SetCharging(true); // 실행할거에요 -> 애니메이터에 차징 시작을 알리기를 + if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true); // 조건이 맞으면 실행할거에요 -> 카메라 줌 효과를 켜기를 } - private void ResetChargingEffects() + private void ResetChargingEffects() // 함수를 선언할거에요 -> 차징 효과를 초기화하는 ResetChargingEffects를 { - _isCharging = false; - _chargeTimer = 0f; - if (pAnim != null) pAnim.SetCharging(false); - if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false); + _isCharging = false; // 상태를 바꿀거에요 -> 차징 상태를 거짓(false)으로 + _chargeTimer = 0f; // 값을 초기화할거에요 -> 차징 타이머를 + if (pAnim != null) pAnim.SetCharging(false); // 실행할거에요 -> 애니메이터 차징 상태 끄기를 + if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false); // 조건이 맞으면 실행할거에요 -> 카메라 줌 효과를 끄기를 } - public void CancelCharging() + public void CancelCharging() // 함수를 선언할거에요 -> 차징을 취소하는 CancelCharging을 { - ResetChargingEffects(); - _waitForRelease = false; + ResetChargingEffects(); // 함수를 실행할거에요 -> 차징 효과 초기화를 + _waitForRelease = false; // 상태를 바꿀거에요 -> 발사 대기 상태를 거짓으로 } - public void ReleaseAttack() + public void ReleaseAttack() // 함수를 선언할거에요 -> 차징된 공격을 발사하는 ReleaseAttack을 { - if (!_isCharging) return; + if (!_isCharging) return; // 조건이 맞으면 중단할거에요 -> 차징 중이 아니라면 - ChargeStage currentStage = chargeStages[0]; - foreach (var stage in chargeStages) + ChargeStage currentStage = chargeStages[0]; // 변수를 초기화할거에요 -> 기본 0단계를 시작값으로 + foreach (var stage in chargeStages) // 반복할거에요 -> 모든 단계를 돌면서 { - if (_chargeTimer >= stage.chargeTime) currentStage = stage; + if (_chargeTimer >= stage.chargeTime) currentStage = stage; // 조건이 맞으면 갱신할거에요 -> 타이머가 단계 시간보다 길다면 해당 단계로 } - float baseDmg = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; - _pendingDamage = baseDmg * currentStage.damageMult; - _pendingSpeed = normalSpeed * currentStage.rangeMult; - _pendingRange = normalRange * currentStage.rangeMult; + float baseDmg = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; // 값을 가져올거에요 -> 기본 공격력을 + _pendingDamage = baseDmg * currentStage.damageMult; // 값을 계산할거에요 -> 배율을 적용한 최종 데미지를 + _pendingSpeed = normalSpeed * currentStage.rangeMult; // 값을 계산할거에요 -> 배율을 적용한 속도를 + _pendingRange = normalRange * currentStage.rangeMult; // 값을 계산할거에요 -> 배율을 적용한 사거리를 - bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f; - _pendingShootDirection = GetShootDirection(isMaxCharge); + bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f; // 조건을 확인할거에요 -> 풀차징(95% 이상)인지 여부를 + _pendingShootDirection = GetShootDirection(isMaxCharge); // 값을 계산할거에요 -> 보정이 적용된 발사 방향을 - if (pAnim != null) pAnim.TriggerThrow(); - _waitForRelease = true; - _lastAttackTime = Time.time; - StartCoroutine(AttackRoutine()); + if (pAnim != null) pAnim.TriggerThrow(); // 실행할거에요 -> 던지는 애니메이션을 + _waitForRelease = true; // 상태를 바꿀거에요 -> 발사 대기 상태를 참으로 + _lastAttackTime = Time.time; // 값을 갱신할거에요 -> 마지막 공격 시간을 + StartCoroutine(AttackRoutine()); // 코루틴을 시작할거에요 -> 공격 루틴을 } // ============================================ // 에임 보정 시스템 — 기존 로직 100% 유지 // ============================================ - private Vector3 GetMouseDirection() + private Vector3 GetMouseDirection() // 함수를 선언할거에요 -> 마우스가 가리키는 방향을 구하는 GetMouseDirection을 { - Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); - Plane firePlane = new Plane(Vector3.up, firePoint.position); + Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 레이를 쏠거에요 -> 카메라에서 마우스 위치로 + Plane firePlane = new Plane(Vector3.up, firePoint.position); // 평면을 만들거에요 -> 발사 위치 높이의 수평면을 - if (firePlane.Raycast(ray, out float distance)) + if (firePlane.Raycast(ray, out float distance)) // 조건이 맞으면 실행할거에요 -> 레이가 평면에 닿았다면 { - Vector3 worldMousePos = ray.GetPoint(distance); - Vector3 direction = (worldMousePos - firePoint.position).normalized; - direction.y = 0; - return direction; + Vector3 worldMousePos = ray.GetPoint(distance); // 위치를 구할거에요 -> 평면상 마우스 위치를 + Vector3 direction = (worldMousePos - firePoint.position).normalized; // 벡터를 구할거에요 -> 발사 위치에서 마우스까지의 방향을 + direction.y = 0; // 값을 바꿀거에요 -> 높이 차이는 무시하게 y를 0으로 + return direction; // 반환할거에요 -> 계산된 방향을 } - return transform.forward; + return transform.forward; // 반환할거에요 -> 실패 시 정면 방향을 } - private Vector3 GetShootDirection(bool isMaxCharge) + private Vector3 GetShootDirection(bool isMaxCharge) // 함수를 선언할거에요 -> 최종 발사 방향(보정 포함)을 구하는 GetShootDirection을 { - Vector3 mouseDir = GetMouseDirection(); - if (!enableAutoAim || (onlyMaxCharge && !isMaxCharge)) return mouseDir; + Vector3 mouseDir = GetMouseDirection(); // 값을 가져올거에요 -> 마우스 방향을 + if (!enableAutoAim || (onlyMaxCharge && !isMaxCharge)) return mouseDir; // 조건이 맞으면 반환할거에요 -> 보정 안 씀 설정이거나 풀차징이 아니면 마우스 방향 그대로 - Transform bestTarget = FindBestTarget(mouseDir); - if (bestTarget != null) + Transform bestTarget = FindBestTarget(mouseDir); // 함수를 실행할거에요 -> 가장 적합한 타겟을 찾는 FindBestTarget을 + if (bestTarget != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 { - Vector3 targetPos = bestTarget.position + Vector3.up * 1.2f; - Vector3 targetDir = (targetPos - firePoint.position).normalized; - targetDir.y = 0; + Vector3 targetPos = bestTarget.position + Vector3.up * 1.2f; // 위치를 계산할거에요 -> 타겟의 중심부(높이 보정)를 + Vector3 targetDir = (targetPos - firePoint.position).normalized; // 방향을 계산할거에요 -> 발사 위치에서 타겟까지 + targetDir.y = 0; // 값을 바꿀거에요 -> 수평 방향만 고려하게 - Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); - assistedDir.Normalize(); - return assistedDir; + Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); // 보간할거에요 -> 마우스 방향과 타겟 방향 사이를 강도만큼 + assistedDir.Normalize(); // 정규화할거에요 -> 벡터 길이를 1로 + return assistedDir; // 반환할거에요 -> 보정된 방향을 } - return mouseDir; + return mouseDir; // 반환할거에요 -> 타겟 없으면 마우스 방향을 } - private Transform FindBestTarget(Vector3 mouseDirection) + private Transform FindBestTarget(Vector3 mouseDirection) // 함수를 선언할거에요 -> 조준 보정 대상을 찾는 FindBestTarget을 { - Collider[] enemies = Physics.OverlapSphere(transform.position, autoAimRange, enemyLayer); - if (enemies.Length == 0) return null; + Collider[] enemies = Physics.OverlapSphere(transform.position, autoAimRange, enemyLayer); // 배열에 담을거에요 -> 사거리 내의 모든 적을 + if (enemies.Length == 0) return null; // 조건이 맞으면 반환할거에요 -> 적이 없으면 null을 - Transform bestTarget = null; - float bestScore = float.MaxValue; + Transform bestTarget = null; // 변수를 초기화할거에요 -> 최고 타겟을 비워두고 + float bestScore = float.MaxValue; // 변수를 초기화할거에요 -> 점수를 최대값으로 - foreach (var enemy in enemies) + foreach (var enemy in enemies) // 반복할거에요 -> 모든 적에 대해 { - Vector3 dirToEnemy = (enemy.transform.position - transform.position).normalized; - float angle = Vector3.Angle(mouseDirection, dirToEnemy); - if (angle > autoAimAngle) continue; + Vector3 dirToEnemy = (enemy.transform.position - transform.position).normalized; // 방향을 구할거에요 -> 적을 향하는 방향을 + float angle = Vector3.Angle(mouseDirection, dirToEnemy); // 각도를 잴거에요 -> 마우스 방향과 적 방향 사이를 + if (angle > autoAimAngle) continue; // 조건이 맞으면 건너뛸거에요 -> 시야각 밖이라면 - float distance = Vector3.Distance(transform.position, enemy.transform.position); - float score = angle * 0.5f + distance * 0.5f; + float distance = Vector3.Distance(transform.position, enemy.transform.position); // 거리를 잴거에요 -> 적까지의 거리를 + float score = angle * 0.5f + distance * 0.5f; // 점수를 매길거에요 -> 각도와 거리를 합산해서 (낮을수록 좋음) - if (score < bestScore) + if (score < bestScore) // 조건이 맞으면 갱신할거에요 -> 현재 점수가 최고 점수보다 낮다면 { - bestScore = score; - bestTarget = enemy.transform; + bestScore = score; // 값을 저장할거에요 -> 새 점수를 + bestTarget = enemy.transform; // 값을 저장할거에요 -> 새 타겟을 } } - return bestTarget; + return bestTarget; // 반환할거에요 -> 가장 점수가 좋은 타겟을 } // ============================================ @@ -276,27 +276,27 @@ public class PlayerAttack : MonoBehaviour /// 애니메이션 이벤트에서 호출됩니다. /// 파티클 프리팹을 Instantiate하고 PlayerArrow 컴포넌트로 이동/충돌을 처리합니다. /// - public void OnShootArrow() + public void OnShootArrow() // 함수를 선언할거에요 -> 애니메이션 이벤트로 호출될 발사 함수 OnShootArrow를 { - if (_currentProjectilePrefab == null || firePoint == null) + if (_currentProjectilePrefab == null || firePoint == null) // 조건이 맞으면 중단할거에요 -> 프리팹이나 발사 위치가 없다면 { - Debug.LogWarning("발사체 프리팹 또는 firePoint가 없습니다!"); + Debug.LogWarning("발사체 프리팹 또는 firePoint가 없습니다!"); // 경고 로그를 띄울거에요 -> 설정 확인 필요 메시지를 return; } // 파티클 프리팹을 발사 위치에 생성 - Quaternion rotation = Quaternion.LookRotation(_pendingShootDirection); - GameObject projectile = Instantiate(_currentProjectilePrefab, firePoint.position, rotation); + Quaternion rotation = Quaternion.LookRotation(_pendingShootDirection); // 회전을 계산할거에요 -> 발사 방향을 바라보게 + GameObject projectile = Instantiate(_currentProjectilePrefab, firePoint.position, rotation); // 생성할거에요 -> 화살 오브젝트를 // PlayerArrow 컴포넌트 확인/추가 후 초기화 - PlayerArrow arrowScript = projectile.GetComponent(); - if (arrowScript == null) + PlayerArrow arrowScript = projectile.GetComponent(); // 컴포넌트를 가져올거에요 -> PlayerArrow 스크립트를 + if (arrowScript == null) // 조건이 맞으면 실행할거에요 -> 스크립트가 없다면 { - arrowScript = projectile.AddComponent(); + arrowScript = projectile.AddComponent(); // 추가할거에요 -> PlayerArrow 컴포넌트를 즉석에서 } // 발사 정보 + 속성 정보 전달 - arrowScript.Initialize( + arrowScript.Initialize( // 초기화할거에요 -> 화살 스크립트에 모든 정보를 전달해서 _pendingDamage, _pendingSpeed, _pendingRange, @@ -311,63 +311,63 @@ public class PlayerAttack : MonoBehaviour // 유틸리티 — 기존 로직 100% 유지 // ============================================ - public void OnAttackEnd() + public void OnAttackEnd() // 함수를 선언할거에요 -> 공격 애니메이션 종료 시 호출될 OnAttackEnd를 { - _isAttacking = false; - ResetChargingEffects(); + _isAttacking = false; // 상태를 바꿀거에요 -> 공격 상태를 거짓으로 + ResetChargingEffects(); // 함수를 실행할거에요 -> 차징 효과 초기화를 } - private IEnumerator AttackRoutine() + private IEnumerator AttackRoutine() // 코루틴 함수를 선언할거에요 -> 공격 상태 안전장치인 AttackRoutine을 { - _isAttacking = true; - yield return new WaitForSeconds(0.6f); - if (_isAttacking) + _isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참으로 + yield return new WaitForSeconds(0.6f); // 기다릴거에요 -> 0.6초(모션 시간)만큼 + if (_isAttacking) // 조건이 맞으면 실행할거에요 -> 아직도 공격 중 상태라면 (강제 종료) { - _isAttacking = false; - ResetChargingEffects(); + _isAttacking = false; // 상태를 바꿀거에요 -> 공격 상태를 거짓으로 + ResetChargingEffects(); // 함수를 실행할거에요 -> 효과 초기화를 } } // [DEPRECATED] 하위 호환성을 위해 남겨둠 - [System.Obsolete("SetCurrentArrow(ArrowData)를 사용하세요.")] - public void SwapArrow(GameObject newArrow) + [System.Obsolete("SetCurrentArrow(ArrowData)를 사용하세요.")] // 경고를 띄울거에요 -> 이 함수는 더 이상 쓰지 말라고 + public void SwapArrow(GameObject newArrow) // 함수를 선언할거에요 -> 구버전 화살 교체 함수 SwapArrow를 { - if (newArrow == null) return; - Debug.LogWarning("SwapArrow()는 더 이상 사용되지 않습니다. SetCurrentArrow()를 사용하세요."); + if (newArrow == null) return; // 조건이 맞으면 중단할거에요 -> 비어있다면 + Debug.LogWarning("SwapArrow()는 더 이상 사용되지 않습니다. SetCurrentArrow()를 사용하세요."); // 경고 로그를 출력할거에요 -> 새 함수 사용 권장을 } - public void StartWeaponCollision() { } - public void StopWeaponCollision() { } + public void StartWeaponCollision() { } // 함수를 비워둘거에요 -> 근접 무기 충돌 시작(활에는 필요 없음) + public void StopWeaponCollision() { } // 함수를 비워둘거에요 -> 근접 무기 충돌 종료(활에는 필요 없음) // ============================================ // Gizmo 디버그 — 기존 로직 100% 유지 // ============================================ - private void OnDrawGizmosSelected() + private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터 선택 시 디버그 그림을 그리는 OnDrawGizmosSelected를 { - if (!enableAutoAim) return; - Gizmos.color = Color.yellow; - Gizmos.DrawWireSphere(transform.position, autoAimRange); + if (!enableAutoAim) return; // 조건이 맞으면 중단할거에요 -> 자동 조준이 꺼져있다면 + Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로 + Gizmos.DrawWireSphere(transform.position, autoAimRange); // 그림을 그릴거에요 -> 자동 조준 범위를 - if (Application.isPlaying && firePoint != null) + if (Application.isPlaying && firePoint != null) // 조건이 맞으면 실행할거에요 -> 게임 실행 중이고 발사 위치가 있다면 { - Vector3 mouseDir = GetMouseDirection(); - Gizmos.color = Color.blue; - Gizmos.DrawRay(firePoint.position, mouseDir * 5f); + Vector3 mouseDir = GetMouseDirection(); // 값을 가져올거에요 -> 마우스 방향을 + Gizmos.color = Color.blue; // 색상을 설정할거에요 -> 파란색으로 + Gizmos.DrawRay(firePoint.position, mouseDir * 5f); // 선을 그릴거에요 -> 마우스 방향 레이를 - Transform target = FindBestTarget(mouseDir); - if (target != null) + Transform target = FindBestTarget(mouseDir); // 값을 가져올거에요 -> 타겟을 + if (target != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 { - Vector3 targetPos = target.position + Vector3.up * 1.2f; - Vector3 targetDir = (targetPos - firePoint.position).normalized; - targetDir.y = 0; - Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); - assistedDir.Normalize(); + Vector3 targetPos = target.position + Vector3.up * 1.2f; // 위치를 계산할거에요 -> 타겟 중심점을 + Vector3 targetDir = (targetPos - firePoint.position).normalized; // 방향을 계산할거에요 -> 타겟 방향을 + targetDir.y = 0; // 값을 바꿀거에요 -> 수평으로 + Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength); // 보간할거에요 -> 보정된 방향을 + assistedDir.Normalize(); // 정규화할거에요 -> 벡터를 - Gizmos.color = Color.red; - Gizmos.DrawRay(firePoint.position, assistedDir * 7f); - Gizmos.color = Color.green; - Gizmos.DrawWireSphere(targetPos, 0.3f); + Gizmos.color = Color.red; // 색상을 설정할거에요 -> 빨간색으로 + Gizmos.DrawRay(firePoint.position, assistedDir * 7f); // 선을 그릴거에요 -> 최종 발사 방향을 + Gizmos.color = Color.green; // 색상을 설정할거에요 -> 초록색으로 + Gizmos.DrawWireSphere(targetPos, 0.3f); // 그림을 그릴거에요 -> 타겟 위치 표시를 } } } diff --git a/Assets/Scripts/Player/Combat/StatusEffectType.cs b/Assets/Scripts/Player/Combat/StatusEffectType.cs index 76370753..693432e0 100644 --- a/Assets/Scripts/Player/Combat/StatusEffectType.cs +++ b/Assets/Scripts/Player/Combat/StatusEffectType.cs @@ -9,6 +9,7 @@ /// public enum StatusEffectType { + None, // ⭐ [추가됨] 아무 상태이상 없음 (기본값) Burn, // 화상 - 일정 시간 동안 틱 데미지 Slow, // 슬로우 - 이동속도 감소 Poison, // 중독 - 틱 데미지 + 방어력 감소 diff --git a/Assets/Scripts/Player/Controller/ArrowPickup.cs b/Assets/Scripts/Player/Controller/ArrowPickup.cs index 46d5c1de..d1e091ee 100644 --- a/Assets/Scripts/Player/Controller/ArrowPickup.cs +++ b/Assets/Scripts/Player/Controller/ArrowPickup.cs @@ -1,228 +1,229 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -/// -/// 바닥에 떨어진 화살 아이템 (아이템 전용 — 발사체 기능 제거됨) -/// 습득 시 PlayerAttack에 파티클 프리팹 + 속성 정보를 전달합니다. -/// -public class ArrowPickup : MonoBehaviour -{ - [Header("--- 화살 아이템 정보 ---")] - [SerializeField] private string arrowName = "기본 화살"; - [SerializeField] private ArrowElementType elementType = ArrowElementType.None; +/// // 요약 주석을 시작할거에요 -> 클래스 설명을 +/// 바닥에 떨어진 화살 아이템 (아이템 전용 — 발사체 기능 제거됨) // 설명할거에요 -> 이 스크립트 역할을 +/// 습득 시 PlayerAttack에 파티클 프리팹 + 속성 정보를 전달합니다. // 설명할거에요 -> 습득 시 전달 내용 +/// // 요약 주석을 끝낼거에요 -> 클래스 설명을 +public class ArrowPickup : MonoBehaviour // 클래스를 선언할거에요 -> 화살 아이템 습득/표시를 담당하는 ArrowPickup을 +{ // 코드 블록을 시작할거에요 -> ArrowPickup 범위를 - [Header("--- 스탯 ---")] - [SerializeField] private float baseDamage = 15f; - [SerializeField] private float elementDamage = 5f; - [SerializeField] private float elementDuration = 3f; + [Header("--- 화살 아이템 정보 ---")] // 인스펙터에 제목을 표시할거에요 -> 화살 아이템 정보 섹션을 + [SerializeField] private string arrowName = "기본 화살"; // 변수를 선언할거에요 -> 화살 이름을 arrowName에 + [SerializeField] private ArrowElementType elementType = ArrowElementType.None; // 변수를 선언할거에요 -> 화살 속성 타입을 elementType에 - [Header("--- 발사체 파티클 ---")] - [Tooltip("이 화살이 장착될 때 발사할 파티클 프리팹")] - [SerializeField] private GameObject projectilePrefab; + [Header("--- 스탯 ---")] // 인스펙터에 제목을 표시할거에요 -> 스탯 섹션을 + [SerializeField] private float baseDamage = 15f; // 변수를 선언할거에요 -> 기본 데미지를 baseDamage에 + [SerializeField] private float elementDamage = 5f; // 변수를 선언할거에요 -> 속성 데미지를 elementDamage에 + [SerializeField] private float elementDuration = 3f; // 변수를 선언할거에요 -> 속성 지속시간을 elementDuration에 - [Header("--- 비주얼 (필드 아이템) ---")] - [SerializeField] private GameObject visualModel; + [Header("--- 발사체 파티클 ---")] // 인스펙터에 제목을 표시할거에요 -> 파티클 섹션을 + [Tooltip("이 화살이 장착될 때 발사할 파티클 프리팹")] // 인스펙터에 툴팁을 달거에요 -> projectilePrefab 설명을 + [SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 장착 시 쓸 파티클 프리팹을 projectilePrefab에 - [Header("--- UI 표시 ---")] - [SerializeField] private GameObject pickupUI; - [SerializeField] private float uiDisplayRange = 3f; + [Header("--- 비주얼 (필드 아이템) ---")] // 인스펙터에 제목을 표시할거에요 -> 비주얼 섹션을 + [SerializeField] private GameObject visualModel; // 변수를 선언할거에요 -> 필드에서 보일 모델을 visualModel에 - [Header("--- Collider 설정 (자동 탐지) ---")] - [Tooltip("아이템 습득용 Sphere Collider (Trigger)")] - [SerializeField] private Collider pickupCollider; - [Tooltip("바닥 충돌용 Box Collider (물리)")] - [SerializeField] private Collider physicsCollider; + [Header("--- UI 표시 ---")] // 인스펙터에 제목을 표시할거에요 -> UI 표시 섹션을 + [SerializeField] private GameObject pickupUI; // 변수를 선언할거에요 -> 습득 UI 오브젝트를 pickupUI에 + [SerializeField] private float uiDisplayRange = 3f; // 변수를 선언할거에요 -> UI 표시 거리를 uiDisplayRange에 - private Transform playerTransform; - private Rigidbody rb; + [Header("--- Collider 설정 (자동 탐지) ---")] // 인스펙터에 제목을 표시할거에요 -> 콜라이더 설정 섹션을 + [Tooltip("아이템 습득용 Sphere Collider (Trigger)")] // 인스펙터에 툴팁을 달거에요 -> pickupCollider 설명을 + [SerializeField] private Collider pickupCollider; // 변수를 선언할거에요 -> 습득용 트리거 콜라이더를 pickupCollider에 + [Tooltip("바닥 충돌용 Box Collider (물리)")] // 인스펙터에 툴팁을 달거에요 -> physicsCollider 설명을 + [SerializeField] private Collider physicsCollider; // 변수를 선언할거에요 -> 물리 충돌 콜라이더를 physicsCollider에 - /// - /// [NEW] ArrowData 구조체로 정보 패키징 - /// - public ArrowData GetArrowData() - { - return new ArrowData - { - arrowName = this.arrowName, - elementType = this.elementType, - baseDamage = this.baseDamage, - elementDamage = this.elementDamage, - elementDuration = this.elementDuration, - projectilePrefab = this.projectilePrefab - }; - } + private Transform playerTransform; // 변수를 선언할거에요 -> 플레이어 트랜스폼을 저장할 playerTransform을 + private Rigidbody rb; // 변수를 선언할거에요 -> 리지드바디를 저장할 rb를 - private void Awake() - { - rb = GetComponent(); + /// // 요약 주석을 시작할거에요 -> ArrowData 패키징 설명을 + /// [NEW] ArrowData 구조체로 정보 패키징 // 설명할거에요 -> PlayerAttack에 넘기기 쉽게 묶는 함수 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + public ArrowData GetArrowData() // 함수를 선언할거에요 -> 현재 화살 정보를 ArrowData로 반환하는 GetArrowData를 + { // 코드 블록을 시작할거에요 -> GetArrowData 범위를 + return new ArrowData // 새 ArrowData를 만들어 반환할거에요 -> 화살 정보를 채워서 + { // 코드 블록을 시작할거에요 -> 초기화 블록 범위를 + arrowName = this.arrowName, // 값을 넣을거에요 -> 화살 이름을 + elementType = this.elementType, // 값을 넣을거에요 -> 속성 타입을 + baseDamage = this.baseDamage, // 값을 넣을거에요 -> 기본 데미지를 + elementDamage = this.elementDamage, // 값을 넣을거에요 -> 속성 데미지를 + elementDuration = this.elementDuration, // 값을 넣을거에요 -> 속성 지속시간을 + projectilePrefab = this.projectilePrefab // 값을 넣을거에요 -> 파티클 프리팹을 + }; // ArrowData 생성을 끝낼거에요 -> 반환 준비 완료 + } // 코드 블록을 끝낼거에요 -> GetArrowData를 - if (pickupCollider == null || physicsCollider == null) - { - AutoDetectColliders(); - } - } + private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + rb = GetComponent(); // 컴포넌트를 가져올거에요 -> Rigidbody를 찾아 rb에 저장 - /// - /// Collider 자동 탐지 (기존 로직 유지) - /// - private void AutoDetectColliders() - { - Collider[] allColliders = GetComponents(); + if (pickupCollider == null || physicsCollider == null) // 조건을 검사할거에요 -> 콜라이더 참조가 비어있는지 + { // 코드 블록을 시작할거에요 -> 자동 탐지 처리 + AutoDetectColliders(); // 함수를 실행할거에요 -> 콜라이더 자동 탐지 + } // 코드 블록을 끝낼거에요 -> 자동 탐지 처리 + } // 코드 블록을 끝낼거에요 -> Awake를 - foreach (Collider col in allColliders) - { - if (col is SphereCollider && pickupCollider == null) - { - pickupCollider = col; - Debug.Log($"습득용 Collider: {col.GetType().Name}"); - } - else if (col is BoxCollider && physicsCollider == null) - { - physicsCollider = col; - Debug.Log($"물리용 Collider: {col.GetType().Name}"); - } - } - } + /// // 요약 주석을 시작할거에요 -> 콜라이더 자동 탐지 설명을 + /// Collider 자동 탐지 (기존 로직 유지) // 설명할거에요 -> Sphere/Box를 찾아 자동 세팅 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + private void AutoDetectColliders() // 함수를 선언할거에요 -> 콜라이더를 자동으로 찾는 AutoDetectColliders를 + { // 코드 블록을 시작할거에요 -> AutoDetectColliders 범위를 + Collider[] allColliders = GetComponents(); // 배열을 가져올거에요 -> 내 오브젝트의 모든 Collider를 - private void Start() - { - SetupAsItem(); - } + foreach (Collider col in allColliders) // 반복할거에요 -> 모든 콜라이더를 하나씩 + { // 코드 블록을 시작할거에요 -> 콜라이더 판별 범위 + if (col is SphereCollider && pickupCollider == null) // 조건을 검사할거에요 -> SphereCollider이고 pickupCollider가 비어있는지 + { // 코드 블록을 시작할거에요 -> 습득용 콜라이더 설정 + pickupCollider = col; // 값을 넣을거에요 -> 습득용 콜라이더로 등록 + Debug.Log($"습득용 Collider: {col.GetType().Name}"); // 로그를 찍을거에요 -> 어떤 타입이 잡혔는지 + } // 코드 블록을 끝낼거에요 -> 습득용 설정 + else if (col is BoxCollider && physicsCollider == null) // 조건을 검사할거에요 -> BoxCollider이고 physicsCollider가 비어있는지 + { // 코드 블록을 시작할거에요 -> 물리용 콜라이더 설정 + physicsCollider = col; // 값을 넣을거에요 -> 물리용 콜라이더로 등록 + Debug.Log($"물리용 Collider: {col.GetType().Name}"); // 로그를 찍을거에요 -> 어떤 타입이 잡혔는지 + } // 코드 블록을 끝낼거에요 -> 물리용 설정 + } // 코드 블록을 끝낼거에요 -> 콜라이더 판별 + } // 코드 블록을 끝낼거에요 -> AutoDetectColliders를 - /// - /// 아이템 모드 설정 (기존 로직 유지 + 단순화) - /// - private void SetupAsItem() - { - GameObject player = GameObject.FindGameObjectWithTag("Player"); - if (player != null) - { - playerTransform = player.transform; - } + private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를 + { // 코드 블록을 시작할거에요 -> Start 범위를 + SetupAsItem(); // 함수를 실행할거에요 -> 아이템 모드로 세팅 + } // 코드 블록을 끝낼거에요 -> Start를 - if (pickupUI != null) pickupUI.SetActive(false); + /// // 요약 주석을 시작할거에요 -> 아이템 모드 세팅 설명을 + /// 아이템 모드 설정 (기존 로직 유지 + 단순화) // 설명할거에요 -> UI/콜라이더/물리 설정을 정리 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + private void SetupAsItem() // 함수를 선언할거에요 -> 아이템 상태로 세팅하는 SetupAsItem을 + { // 코드 블록을 시작할거에요 -> SetupAsItem 범위를 + GameObject player = GameObject.FindGameObjectWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그 오브젝트를 + if (player != null) // 조건을 검사할거에요 -> 플레이어를 찾았는지 + { // 코드 블록을 시작할거에요 -> 플레이어 참조 저장 + playerTransform = player.transform; // 값을 넣을거에요 -> 플레이어 위치를 저장 + } // 코드 블록을 끝낼거에요 -> 플레이어 참조 저장 - // Collider 설정 - if (pickupCollider != null) - { - pickupCollider.enabled = true; - pickupCollider.isTrigger = true; - } + if (pickupUI != null) pickupUI.SetActive(false); // 조건이 맞으면 실행할거에요 -> 시작 시 UI를 숨기기 - if (physicsCollider != null) - { - physicsCollider.enabled = true; - physicsCollider.isTrigger = false; - } + // Collider 설정 // 설명을 적을거에요 -> 습득용/물리용 콜라이더 모드 설정 + if (pickupCollider != null) // 조건을 검사할거에요 -> 습득용 콜라이더가 있는지 + { // 코드 블록을 시작할거에요 -> 습득용 콜라이더 설정 + pickupCollider.enabled = true; // 값을 바꿀거에요 -> 습득용 콜라이더 활성화 + pickupCollider.isTrigger = true; // 값을 바꿀거에요 -> 습득용은 트리거로 + } // 코드 블록을 끝낼거에요 -> 습득용 설정 - // 물리 설정 - if (rb != null) - { - rb.useGravity = true; - rb.isKinematic = false; - rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; - } - } + if (physicsCollider != null) // 조건을 검사할거에요 -> 물리용 콜라이더가 있는지 + { // 코드 블록을 시작할거에요 -> 물리용 콜라이더 설정 + physicsCollider.enabled = true; // 값을 바꿀거에요 -> 물리 콜라이더 활성화 + physicsCollider.isTrigger = false; // 값을 바꿀거에요 -> 물리용은 트리거 아님 + } // 코드 블록을 끝낼거에요 -> 물리용 설정 - private void Update() - { - if (playerTransform == null || pickupUI == null) return; + // 물리 설정 // 설명을 적을거에요 -> 리지드바디 세팅 + if (rb != null) // 조건을 검사할거에요 -> Rigidbody가 있는지 + { // 코드 블록을 시작할거에요 -> Rigidbody 설정 + rb.useGravity = true; // 값을 바꿀거에요 -> 중력 적용 + rb.isKinematic = false; // 값을 바꿀거에요 -> 물리 시뮬레이션 적용 + rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 값을 바꿀거에요 -> 빠른 충돌도 놓치지 않게 + } // 코드 블록을 끝낼거에요 -> Rigidbody 설정 + } // 코드 블록을 끝낼거에요 -> SetupAsItem을 - float distance = Vector3.Distance(transform.position, playerTransform.position); - pickupUI.SetActive(distance <= uiDisplayRange); - } + private void Update() // 함수를 선언할거에요 -> 매 프레임 실행되는 Update를 + { // 코드 블록을 시작할거에요 -> Update 범위를 + if (playerTransform == null || pickupUI == null) return; // 조건이 맞으면 종료할거에요 -> 플레이어/UI가 없으면 처리 불가 - /// - /// [MODIFIED] PlayerInteraction에서 'E' 키를 눌렀을 때 호출 - /// 기존: SwapArrow(gameObject) → 변경: SetCurrentArrow(ArrowData) - /// - public void Pickup(PlayerAttack playerAttack) - { - if (playerAttack == null) return; + float distance = Vector3.Distance(transform.position, playerTransform.position); // 값을 계산할거에요 -> 플레이어와의 거리를 + pickupUI.SetActive(distance <= uiDisplayRange); // 값을 적용할거에요 -> 거리 안이면 UI 켜고 밖이면 끄기 + } // 코드 블록을 끝낼거에요 -> Update를 - // ArrowData를 패키징하여 PlayerAttack에 전달 - ArrowData data = GetArrowData(); - playerAttack.SetCurrentArrow(data); + /// // 요약 주석을 시작할거에요 -> 습득 함수 설명을 + /// [MODIFIED] PlayerInteraction에서 'E' 키를 눌렀을 때 호출 // 설명할거에요 -> 상호작용으로 호출되는 함수 + /// 기존: SwapArrow(gameObject) → 변경: SetCurrentArrow(ArrowData) // 설명할거에요 -> 전달 방식 변경점 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + public void Pickup(PlayerAttack playerAttack) // 함수를 선언할거에요 -> 플레이어가 화살을 줍는 Pickup을 + { // 코드 블록을 시작할거에요 -> Pickup 범위를 + if (playerAttack == null) return; // 조건이 맞으면 종료할거에요 -> PlayerAttack이 없으면 진행 불가 - Debug.Log($"[{arrowName}] 화살 습득! 속성: {elementType}"); + ArrowData data = GetArrowData(); // 값을 만들거에요 -> 현재 화살 정보를 ArrowData로 패키징 + playerAttack.SetCurrentArrow(data); // 함수를 실행할거에요 -> 플레이어 공격 스크립트에 현재 화살로 세팅 - // 아이템 제거 - Destroy(gameObject); - } + Debug.Log($"[{arrowName}] 화살 습득! 속성: {elementType}"); // 로그를 찍을거에요 -> 습득/속성 확인 - /// - /// 몬스터 드롭 시 화살 정보 설정 (기존 함수 유지) - /// - public void SetArrowData(string name, float dmg, float spd, float rng) - { - arrowName = name; - baseDamage = dmg; - } + Destroy(gameObject); // 오브젝트를 삭제할거에요 -> 습득했으니 아이템 제거 + } // 코드 블록을 끝낼거에요 -> Pickup을 - /// - /// [NEW] 속성 정보까지 포함한 전체 세팅 함수 - /// - public void SetArrowDataFull( - string name, - ArrowElementType element, - float baseDmg, - float elemDmg, - float elemDur, - GameObject particlePrefab) - { - arrowName = name; - elementType = element; - baseDamage = baseDmg; - elementDamage = elemDmg; - elementDuration = elemDur; - projectilePrefab = particlePrefab; - } + /// // 요약 주석을 시작할거에요 -> 드롭 세팅 함수 설명을 + /// 몬스터 드롭 시 화살 정보 설정 (기존 함수 유지) // 설명할거에요 -> 예전 호환용 함수 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + public void SetArrowData(string name, float dmg, float spd, float rng) // 함수를 선언할거에요 -> 간단 세팅용 SetArrowData를 + { // 코드 블록을 시작할거에요 -> SetArrowData 범위를 + arrowName = name; // 값을 넣을거에요 -> 이름을 세팅 + baseDamage = dmg; // 값을 넣을거에요 -> 기본 데미지를 세팅 + } // 코드 블록을 끝낼거에요 -> SetArrowData를 - /// - /// 바닥 충돌 시 속도 감쇠 (기존 로직 유지) - /// - private void OnCollisionEnter(Collision collision) - { - if (rb != null && collision.gameObject.CompareTag("Ground")) - { - rb.velocity *= 0.5f; - rb.angularVelocity *= 0.5f; - } - } + /// // 요약 주석을 시작할거에요 -> 전체 세팅 함수 설명을 + /// [NEW] 속성 정보까지 포함한 전체 세팅 함수 // 설명할거에요 -> 속성/파티클까지 세팅하는 버전 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + public void SetArrowDataFull( // 함수를 선언할거에요 -> 화살 모든 정보를 세팅하는 SetArrowDataFull을 + string name, // 매개변수를 받을거에요 -> 화살 이름을 + ArrowElementType element, // 매개변수를 받을거에요 -> 속성 타입을 + float baseDmg, // 매개변수를 받을거에요 -> 기본 데미지를 + float elemDmg, // 매개변수를 받을거에요 -> 속성 데미지를 + float elemDur, // 매개변수를 받을거에요 -> 속성 지속 시간을 + GameObject particlePrefab) // 매개변수를 받을거에요 -> 파티클 프리팹을 + { // 코드 블록을 시작할거에요 -> SetArrowDataFull 범위를 + arrowName = name; // 값을 넣을거에요 -> 이름 세팅 + elementType = element; // 값을 넣을거에요 -> 속성 세팅 + baseDamage = baseDmg; // 값을 넣을거에요 -> 기본 데미지 세팅 + elementDamage = elemDmg; // 값을 넣을거에요 -> 속성 데미지 세팅 + elementDuration = elemDur; // 값을 넣을거에요 -> 속성 지속시간 세팅 + projectilePrefab = particlePrefab; // 값을 넣을거에요 -> 파티클 프리팹 세팅 + } // 코드 블록을 끝낼거에요 -> SetArrowDataFull을 - #region 디버그 시각화 + /// // 요약 주석을 시작할거에요 -> 바닥 충돌 감쇠 설명을 + /// 바닥 충돌 시 속도 감쇠 (기존 로직 유지) // 설명할거에요 -> 바닥에 튕김을 줄이기 + /// // 요약 주석을 끝낼거에요 -> 설명 종료 + private void OnCollisionEnter(Collision collision) // 함수를 선언할거에요 -> 충돌이 발생하면 호출되는 OnCollisionEnter를 + { // 코드 블록을 시작할거에요 -> OnCollisionEnter 범위를 + if (rb != null && collision.gameObject.CompareTag("Ground")) // 조건을 검사할거에요 -> Rigidbody가 있고 땅에 닿았는지 + { // 코드 블록을 시작할거에요 -> 감쇠 처리 + rb.velocity *= 0.5f; // 값을 줄일거에요 -> 선속도를 절반으로 + rb.angularVelocity *= 0.5f; // 값을 줄일거에요 -> 회전속도를 절반으로 + } // 코드 블록을 끝낼거에요 -> 감쇠 처리 + } // 코드 블록을 끝낼거에요 -> OnCollisionEnter를 - private void OnDrawGizmosSelected() - { - if (pickupCollider != null && pickupCollider.enabled) - { - Gizmos.color = Color.green; - if (pickupCollider is SphereCollider sphere) - { - Gizmos.DrawWireSphere(transform.position + sphere.center, sphere.radius); - } - } + #region 디버그 시각화 // 영역을 표시할거에요 -> Gizmos 관련 코드 묶음임을 - if (physicsCollider != null && physicsCollider.enabled) - { - Gizmos.color = Color.blue; - if (physicsCollider is BoxCollider box) - { - Gizmos.matrix = transform.localToWorldMatrix; - Gizmos.DrawWireCube(box.center, box.size); - } - } + private void OnDrawGizmosSelected() // 함수를 선언할거에요 -> 선택됐을 때 Gizmo를 그리는 OnDrawGizmosSelected를 + { // 코드 블록을 시작할거에요 -> OnDrawGizmosSelected 범위를 + if (pickupCollider != null && pickupCollider.enabled) // 조건을 검사할거에요 -> 습득용 콜라이더가 있고 켜져있는지 + { // 코드 블록을 시작할거에요 -> 습득 범위 시각화 + Gizmos.color = Color.green; // 색을 정할거에요 -> 초록색으로 + if (pickupCollider is SphereCollider sphere) // 조건을 검사할거에요 -> 습득용이 SphereCollider인지 + { // 코드 블록을 시작할거에요 -> 구체 범위 그리기 + Gizmos.DrawWireSphere(transform.position + sphere.center, sphere.radius); // 선 구체를 그릴거에요 -> 습득 범위를 + } // 코드 블록을 끝낼거에요 -> 구체 그리기 + } // 코드 블록을 끝낼거에요 -> 습득 범위 시각화 - // [NEW] 속성 색상 표시 - switch (elementType) - { - case ArrowElementType.Fire: Gizmos.color = Color.red; break; - case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; - case ArrowElementType.Poison: Gizmos.color = new Color(0.5f, 1f, 0f); break; - case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; - default: Gizmos.color = Color.white; break; - } - Gizmos.DrawWireSphere(transform.position, 0.3f); - } + if (physicsCollider != null && physicsCollider.enabled) // 조건을 검사할거에요 -> 물리 콜라이더가 있고 켜져있는지 + { // 코드 블록을 시작할거에요 -> 물리 범위 시각화 + Gizmos.color = Color.blue; // 색을 정할거에요 -> 파란색으로 + if (physicsCollider is BoxCollider box) // 조건을 검사할거에요 -> 물리용이 BoxCollider인지 + { // 코드 블록을 시작할거에요 -> 박스 범위 그리기 + Gizmos.matrix = transform.localToWorldMatrix; // 좌표계를 바꿀거에요 -> 오브젝트 로컬 기준으로 + Gizmos.DrawWireCube(box.center, box.size); // 선 박스를 그릴거에요 -> 물리 충돌 범위를 + } // 코드 블록을 끝낼거에요 -> 박스 그리기 + } // 코드 블록을 끝낼거에요 -> 물리 범위 시각화 - #endregion -} \ No newline at end of file + // [NEW] 속성 색상 표시 // 설명을 적을거에요 -> 속성 타입에 따라 색을 다르게 + switch (elementType) // 분기할거에요 -> elementType에 따라 + { // 코드 블록을 시작할거에요 -> switch 범위를 + case ArrowElementType.Fire: Gizmos.color = Color.red; break; // 빨강으로 칠할거에요 -> 불 속성이면 + case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; // 하늘색으로 칠할거에요 -> 얼음 속성이면 + case ArrowElementType.Poison: Gizmos.color = new Color(0.5f, 1f, 0f); break; // 연두로 칠할거에요 -> 독 속성이면 + case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; // 노랑으로 칠할거에요 -> 번개 속성이면 + default: Gizmos.color = Color.white; break; // 흰색으로 칠할거에요 -> 기본/없음이면 + } // 코드 블록을 끝낼거에요 -> switch를 + + Gizmos.DrawWireSphere(transform.position, 0.3f); // 선 구체를 그릴거에요 -> 아이템 위치에 속성 표시용으로 + } // 코드 블록을 끝낼거에요 -> OnDrawGizmosSelected를 + + #endregion // 영역을 끝낼거에요 -> Gizmos 묶음을 + +} // 코드 블록을 끝낼거에요 -> ArrowPickup을 \ No newline at end of file diff --git a/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs b/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs index 44cb5543..d800971f 100644 --- a/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs +++ b/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs @@ -1,43 +1,42 @@ -using System.Collections.Generic; -using UnityEngine; +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 /// /// 플레이어의 전투 행동을 슬라이딩 윈도우(최근 N초) 기반으로 추적합니다. -/// 보스 AI가 이 데이터를 읽어 카운터 패턴 발동 여부를 판단합니다. /// -public class PlayerBehaviorTracker : MonoBehaviour +public class PlayerBehaviorTracker : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerBehaviorTracker를 { - public static PlayerBehaviorTracker Instance { get; private set; } + public static PlayerBehaviorTracker Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를 - [Header("슬라이딩 윈도우 설정")] - [Tooltip("행동 추적 윈도우 크기(초)")] - [SerializeField] private float windowDuration = 10f; + [Header("슬라이딩 윈도우 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 슬라이딩 윈도우 설정 을 + [Tooltip("행동 추적 윈도우 크기(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float windowDuration = 10f; // 변수를 선언할거에요 -> 추적 시간 범위(10초)를 windowDuration에 // ── 내부 기록용 타임스탬프 리스트 ── - private List dodgeTimestamps = new List(); - private List aimStartTimes = new List(); - private List aimEndTimes = new List(); - private List pierceShotTimestamps = new List(); - private List totalShotTimestamps = new List(); + private List dodgeTimestamps = new List(); // 리스트를 만들거에요 -> 회피 시간들을 저장할 dodgeTimestamps를 + private List aimStartTimes = new List(); // 리스트를 만들거에요 -> 조준 시작 시간들을 저장할 aimStartTimes를 + private List aimEndTimes = new List(); // 리스트를 만들거에요 -> 조준 종료 시간들을 저장할 aimEndTimes를 + private List pierceShotTimestamps = new List(); // 리스트를 만들거에요 -> 관통샷 시간들을 저장할 pierceShotTimestamps를 + private List totalShotTimestamps = new List(); // 리스트를 만들거에요 -> 전체 사격 시간들을 저장할 totalShotTimestamps를 // ── 현재 조준 상태 ── - private bool isAiming = false; - private float currentAimStartTime; + private bool isAiming = false; // 변수를 초기화할거에요 -> 조준 중 여부를 거짓(false)으로 + private float currentAimStartTime; // 변수를 선언할거에요 -> 현재 조준 시작 시간을 저장할 currentAimStartTime을 // ── 외부에서 읽는 프로퍼티 ── - public int DodgeCount => CountInWindow(dodgeTimestamps); - public float AimHoldTime => CalculateAimHoldTime(); - public float PierceRatio => CalculatePierceRatio(); - public int TotalShotsInWindow => CountInWindow(totalShotTimestamps); + public int DodgeCount => CountInWindow(dodgeTimestamps); // 값을 반환할거에요 -> 최근 회피 횟수를 계산해서 + public float AimHoldTime => CalculateAimHoldTime(); // 값을 반환할거에요 -> 최근 조준 유지 시간을 계산해서 + public float PierceRatio => CalculatePierceRatio(); // 값을 반환할거에요 -> 최근 관통샷 비율을 계산해서 + public int TotalShotsInWindow => CountInWindow(totalShotTimestamps); // 값을 반환할거에요 -> 최근 총 발사 횟수를 계산해서 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를 { - if (Instance != null && Instance != this) + if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면 { - Destroy(gameObject); - return; + Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를 + return; // 중단할거에요 -> 초기화 로직을 } - Instance = this; + Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에 } // ═══════════════════════════════════════════ @@ -45,39 +44,39 @@ public class PlayerBehaviorTracker : MonoBehaviour // ═══════════════════════════════════════════ /// 플레이어가 회피(대시/구르기)를 수행했을 때 호출 - public void RecordDodge() + public void RecordDodge() // 함수를 선언할거에요 -> 회피를 기록하는 RecordDodge를 { - dodgeTimestamps.Add(Time.time); + dodgeTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 회피 목록에 } /// 플레이어가 조준을 시작했을 때 호출 - public void RecordAimStart() + public void RecordAimStart() // 함수를 선언할거에요 -> 조준 시작을 기록하는 RecordAimStart를 { - if (!isAiming) + if (!isAiming) // 조건이 맞으면 실행할거에요 -> 이미 조준 중이 아니라면 { - isAiming = true; - currentAimStartTime = Time.time; + isAiming = true; // 상태를 바꿀거에요 -> 조준 중 상태를 참(true)으로 + currentAimStartTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 조준 시작 시간으로 } } /// 플레이어가 조준을 해제했을 때 호출 - public void RecordAimEnd() + public void RecordAimEnd() // 함수를 선언할거에요 -> 조준 종료를 기록하는 RecordAimEnd를 { - if (isAiming) + if (isAiming) // 조건이 맞으면 실행할거에요 -> 조준 중이었다면 { - isAiming = false; - aimStartTimes.Add(currentAimStartTime); - aimEndTimes.Add(Time.time); + isAiming = false; // 상태를 바꿀거에요 -> 조준 중 상태를 거짓(false)으로 + aimStartTimes.Add(currentAimStartTime); // 추가할거에요 -> 시작 시간을 리스트에 + aimEndTimes.Add(Time.time); // 추가할거에요 -> 종료(현재) 시간을 리스트에 } } /// 화살 발사 시 호출. isPierce=true면 관통 화살 - public void RecordShot(bool isPierce) + public void RecordShot(bool isPierce) // 함수를 선언할거에요 -> 발사를 기록하는 RecordShot을 { - totalShotTimestamps.Add(Time.time); - if (isPierce) + totalShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 전체 사격 목록에 + if (isPierce) // 조건이 맞으면 실행할거에요 -> 관통 화살이라면 { - pierceShotTimestamps.Add(Time.time); + pierceShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 관통 사격 목록에 } } @@ -86,62 +85,61 @@ public class PlayerBehaviorTracker : MonoBehaviour // ═══════════════════════════════════════════ /// 새 런 시작 시 모든 행동 기록을 초기화 - public void ResetForNewRun() + public void ResetForNewRun() // 함수를 선언할거에요 -> 모든 기록을 초기화하는 ResetForNewRun을 { - dodgeTimestamps.Clear(); - aimStartTimes.Clear(); - aimEndTimes.Clear(); - pierceShotTimestamps.Clear(); - totalShotTimestamps.Clear(); - isAiming = false; + dodgeTimestamps.Clear(); // 비울거에요 -> 회피 기록을 + aimStartTimes.Clear(); // 비울거에요 -> 조준 시작 기록을 + aimEndTimes.Clear(); // 비울거에요 -> 조준 종료 기록을 + pierceShotTimestamps.Clear(); // 비울거에요 -> 관통 사격 기록을 + totalShotTimestamps.Clear(); // 비울거에요 -> 전체 사격 기록을 + isAiming = false; // 초기화할거에요 -> 조준 상태를 거짓(false)으로 } // ═══════════════════════════════════════════ // 내부 계산 // ═══════════════════════════════════════════ - private int CountInWindow(List timestamps) + private int CountInWindow(List timestamps) // 함수를 선언할거에요 -> 윈도우 내 개수를 세는 CountInWindow를 { - float cutoff = Time.time - windowDuration; - // 오래된 항목 제거 - timestamps.RemoveAll(t => t < cutoff); - return timestamps.Count; + float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 현재 시간에서 윈도우 크기를 뺀 기준 시간을 + timestamps.RemoveAll(t => t < cutoff); // 삭제할거에요 -> 기준 시간보다 오래된 기록들을 리스트에서 + return timestamps.Count; // 반환할거에요 -> 남은 기록의 개수를 } - private float CalculateAimHoldTime() + private float CalculateAimHoldTime() // 함수를 선언할거에요 -> 총 조준 시간을 계산하는 CalculateAimHoldTime을 { - float cutoff = Time.time - windowDuration; - float total = 0f; + float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 기준 시간(현재 - 윈도우)을 + float total = 0f; // 변수를 초기화할거에요 -> 총 시간을 0으로 // 완료된 조준 세션 - for (int i = aimStartTimes.Count - 1; i >= 0; i--) + for (int i = aimStartTimes.Count - 1; i >= 0; i--) // 반복할거에요 -> 저장된 조준 기록들을 역순으로 { - if (aimEndTimes[i] < cutoff) + if (aimEndTimes[i] < cutoff) // 조건이 맞으면 실행할거에요 -> 종료 시간이 기준보다 옛날이라면 { - aimStartTimes.RemoveAt(i); - aimEndTimes.RemoveAt(i); - continue; + aimStartTimes.RemoveAt(i); // 삭제할거에요 -> 해당 시작 기록을 + aimEndTimes.RemoveAt(i); // 삭제할거에요 -> 해당 종료 기록을 + continue; // 건너뛸거에요 -> 다음 반복으로 } - float start = Mathf.Max(aimStartTimes[i], cutoff); - total += aimEndTimes[i] - start; + float start = Mathf.Max(aimStartTimes[i], cutoff); // 값을 계산할거에요 -> 시작 시간과 기준 시간 중 더 최근 것을 + total += aimEndTimes[i] - start; // 값을 더할거에요 -> 유효한 조준 기간을 총 시간에 } // 현재 조준 중인 시간도 포함 - if (isAiming) + if (isAiming) // 조건이 맞으면 실행할거에요 -> 현재 조준 중이라면 { - float start = Mathf.Max(currentAimStartTime, cutoff); - total += Time.time - start; + float start = Mathf.Max(currentAimStartTime, cutoff); // 값을 계산할거에요 -> 조준 시작과 기준 중 최근 것을 + total += Time.time - start; // 값을 더할거에요 -> 지금까지의 조준 시간을 총 시간에 } - return total; + return total; // 반환할거에요 -> 계산된 총 조준 시간을 } - private float CalculatePierceRatio() + private float CalculatePierceRatio() // 함수를 선언할거에요 -> 관통 비율을 계산하는 CalculatePierceRatio를 { - int totalShots = CountInWindow(totalShotTimestamps); - if (totalShots == 0) return 0f; + int totalShots = CountInWindow(totalShotTimestamps); // 값을 가져올거에요 -> 최근 총 발사 횟수를 + if (totalShots == 0) return 0f; // 조건이 맞으면 반환할거에요 -> 발사 기록이 없으면 0을 - int pierceShots = CountInWindow(pierceShotTimestamps); - return (float)pierceShots / totalShots; + int pierceShots = CountInWindow(pierceShotTimestamps); // 값을 가져올거에요 -> 최근 관통 발사 횟수를 + return (float)pierceShots / totalShots; // 반환할거에요 -> 관통 횟수 나누기 전체 횟수(비율)를 } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Player/Controller/PlayerInput.cs b/Assets/Scripts/Player/Controller/PlayerInput.cs index 2d3da6fc..6e517429 100644 --- a/Assets/Scripts/Player/Controller/PlayerInput.cs +++ b/Assets/Scripts/Player/Controller/PlayerInput.cs @@ -1,67 +1,67 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class PlayerInput : MonoBehaviour +public class PlayerInput : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerInput을 { // 다른 스크립트에게 명령을 내려야 하니, 미리 참조 시킴 - [SerializeField] private PlayerHealth health; // 죽었는지 확인용 - [SerializeField] private PlayerMovement movement; // 이동 명령용 - [SerializeField] private PlayerAim aim; // 회전 명령용 - [SerializeField] private PlayerInteraction interaction; // 아이템 줍기 명령용 - [SerializeField] private PlayerAttack attack; - [SerializeField] private PlayerStatsUI statsUI; // UI 창 열기 명령용 + [SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 죽었는지 확인할 health를 + [SerializeField] private PlayerMovement movement; // 변수를 선언할거에요 -> 이동 명령을 내릴 movement를 + [SerializeField] private PlayerAim aim; // 변수를 선언할거에요 -> 회전 명령을 내릴 aim을 + [SerializeField] private PlayerInteraction interaction; // 변수를 선언할거에요 -> 상호작용 명령을 내릴 interaction을 + [SerializeField] private PlayerAttack attack; // 변수를 선언할거에요 -> 공격 명령을 내릴 attack을 + [SerializeField] private PlayerStatsUI statsUI; // 변수를 선언할거에요 -> UI 토글을 위한 statsUI를 - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를 { // 1. 사망 체크 // 죽었는데 키보드 눌린다고 시체가 움직이면 안 되니까 아예 입력을 차단(return)함 - if (health != null && health.IsDead) return; + if (health != null && health.IsDead) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 죽었다면 // 2. UI 토글 (C키) // 스탯 창을 껐다 켰다 함 - if (Input.GetKeyDown(KeyCode.C) && statsUI != null) statsUI.ToggleWindow(); + if (Input.GetKeyDown(KeyCode.C) && statsUI != null) statsUI.ToggleWindow(); // 조건이 맞으면 실행할거에요 -> C키를 눌렀다면 스탯창 토글을 // 3. 이동 입력 감지 // GetAxisRaw를 쓴 이유: 0에서 1로 부드럽게 변하는 게 아니라, // 키를 누르면 즉시 1, 떼면 0이 되어서 빠릿빠릿한 조작감을 줌 - float h = Input.GetAxisRaw("Horizontal"); // A, D 키 (-1, 0, 1) - float v = Input.GetAxisRaw("Vertical"); // W, S 키 (-1, 0, 1) - bool sprint = Input.GetKey(KeyCode.LeftShift); // Shift 누르고 있나? + float h = Input.GetAxisRaw("Horizontal"); // 값을 가져올거에요 -> 좌우 입력값(-1, 0, 1)을 h에 + float v = Input.GetAxisRaw("Vertical"); // 값을 가져올거에요 -> 상하 입력값(-1, 0, 1)을 v에 + bool sprint = Input.GetKey(KeyCode.LeftShift); // 값을 가져올거에요 -> 쉬프트 키 입력 여부를 sprint에 // 4. 이동 명령 하달 - if (movement != null) + if (movement != null) // 조건이 맞으면 실행할거에요 -> 이동 스크립트가 있다면 { // 방향 벡터를 정규화(.normalized)해서 대각선 이동 시 빨라지는 걸 방지 // 그리고 달리기 여부(sprint)도 같이 넘겨줍니다. - movement.SetMoveInput(new Vector3(h, 0, v).normalized, sprint); + movement.SetMoveInput(new Vector3(h, 0, v).normalized, sprint); // 실행할거에요 -> 이동 입력 전달 함수를 // ⭐ [추가] 스페이스바 누르면 대시 명령! - if (Input.GetKeyDown(KeyCode.Space)) movement.AttemptDash(); + if (Input.GetKeyDown(KeyCode.Space)) movement.AttemptDash(); // 조건이 맞으면 실행할거에요 -> 스페이스바 누르면 대시 시도를 } // 5. 마우스 회전 명령 // 캐릭터가 마우스 커서를 바라보게 합니다. - if (aim != null) aim.RotateTowardsMouse(); + if (aim != null) aim.RotateTowardsMouse(); // 조건이 맞으면 실행할거에요 -> 마우스 방향 회전 함수를 // 6. 상호작용 (F키) // 바닥에 떨어진 무기를 줍기 - if (Input.GetKeyDown(KeyCode.F) && interaction != null) interaction.TryInteract(); + if (Input.GetKeyDown(KeyCode.F) && interaction != null) interaction.TryInteract(); // 조건이 맞으면 실행할거에요 -> F키 누르면 상호작용 시도를 // 7. 공격 입력 (좌클릭/우클릭) - if (attack != null) + if (attack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면 { // 우클릭 꾹: 차징(모으기) 시작 - if (Input.GetMouseButtonDown(1)) attack.StartCharging(); + if (Input.GetMouseButtonDown(1)) attack.StartCharging(); // 조건이 맞으면 실행할거에요 -> 우클릭 누르면 차징 시작을 // 우클릭 뗌: 차징 취소 (공격 안 하고 캔슬) - if (Input.GetMouseButtonUp(1)) attack.CancelCharging(); + if (Input.GetMouseButtonUp(1)) attack.CancelCharging(); // 조건이 맞으면 실행할거에요 -> 우클릭 떼면 차징 취소를 // 좌클릭: 공격 시도 - if (Input.GetMouseButtonDown(0)) + if (Input.GetMouseButtonDown(0)) // 조건이 맞으면 실행할거에요 -> 좌클릭을 눌렀다면 { // 만약 우클릭(방어/조준) 상태에서 좌클릭을 했다면? -> 투척(Release) - if (Input.GetMouseButton(1)) attack.ReleaseAttack(); + if (Input.GetMouseButton(1)) attack.ReleaseAttack(); // 조건이 맞으면 실행할거에요 -> 차징 중이라면 발사를 // 그냥 좌클릭만 했다면? -> 일반 평타(Normal Attack) - else attack.PerformNormalAttack(); + else attack.PerformNormalAttack(); // 그 외엔 실행할거에요 -> 일반 공격을 } } } diff --git a/Assets/Scripts/Player/Controller/PlayerMovement.cs b/Assets/Scripts/Player/Controller/PlayerMovement.cs index e70f7fbf..61bd33fa 100644 --- a/Assets/Scripts/Player/Controller/PlayerMovement.cs +++ b/Assets/Scripts/Player/Controller/PlayerMovement.cs @@ -1,277 +1,216 @@ -using UnityEngine; -using System.Collections; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 - -[RequireComponent(typeof(CharacterController))] // 이 스크립트를 넣으면 CharacterController도 자동으로 추가됨 (실수 방지) -public class PlayerMovement : MonoBehaviour +[RequireComponent(typeof(CharacterController))] // 컴포넌트를 강제로 추가할거에요 -> CharacterController를 (이 스크립트를 넣으면 자동으로) +public class PlayerMovement : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerMovement를 { - [Header("=== 참조 ===")] - // 외부 스크립트들을 상속 시키기 위한 변수들 - // 'private'에 [SerializeField]를 붙여 에디터 인스펙터 창에서 설정 가능 - [SerializeField] private Stats stats; // 이동 속도(Stat)를 가져오기 위해 필요 - [SerializeField] private PlayerHealth health; // 죽거나 맞았을 때 이동을 멈추기 위해 필요 - [SerializeField] private PlayerAnimator pAnim; // 이동에 맞춰 뛰는 모션을 재생하기 위해 필요 - [SerializeField] private PlayerAttack attackScript; // 공격 중일 때 이동을 막기 위해 필요 + [Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 참조 === 를 - [Header("=== CharacterController ===")] - //CharacterControlle는 유니티가 제공하는 인간형 이동 컴포넌트 - // Rigidbody와 달리 경사면, 계단 처리가 자동으로 됨. - private CharacterController _controller; + [SerializeField] private Stats stats; // 변수를 선언하고 인스펙터에 노출할거에요 -> 이동 속도 정보를 가진 Stats 스크립트를 + [SerializeField] private PlayerHealth health; // 변수를 선언하고 인스펙터에 노출할거에요 -> 사망 및 피격 상태를 알기 위한 PlayerHealth를 + [SerializeField] private PlayerAnimator pAnim; // 변수를 선언하고 인스펙터에 노출할거에요 -> 애니메이션을 제어할 PlayerAnimator를 + [SerializeField] private PlayerAttack attackScript; // 변수를 선언하고 인스펙터에 노출할거에요 -> 공격 상태를 알기 위한 PlayerAttack을 - [Header("=== 대시 설정 ===")] - [SerializeField] private float dashDistance = 3f; // 대시로 이동할 총 거리 - [SerializeField] private float dashDuration = 0.08f; // 대시가 끝나는 시간 (짧을수록 빠름) - [SerializeField] private float dashCooldown = 1.5f; // 대시 후 다시 쓰기까지 기다리는 시간 + [Header("=== CharacterController ===")] // 인스펙터 창에 제목을 표시할거에요 -> === CharacterController === 를 + private CharacterController _controller; // 변수를 선언할거에요 -> 이동을 담당할 CharacterController 컴포넌트를 담을 _controller를 - [Header("=== 차징 감속 설정 ===")] - [Range(0.1f, 1f)] // 슬라이더로 조절하게 만듦 (1이면 감속 없음, 0.1이면 엄청 느려짐) - [SerializeField] private float minSpeedMultiplier = 0.3f; // 차징 중일 때 속도를 30%로 줄임 + [Header("=== 대시 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 대시 설정 === 을 + [SerializeField] private float dashDistance = 3f; // 변수를 선언할거에요 -> 대시로 이동할 거리(3.0)를 dashDistance에 + [SerializeField] private float dashDuration = 0.08f; // 변수를 선언할거에요 -> 대시가 지속될 시간(0.08초)을 dashDuration에 + [SerializeField] private float dashCooldown = 1.5f; // 변수를 선언할거에요 -> 대시 재사용 대기시간(1.5초)을 dashCooldown에 - [Header("=== 중력 설정 ===")] - [SerializeField] private float gravity = -20f; // 지구 중력(-9.81)보다 세게 해서 점프 후 빨리 떨어지게 함 (조작감 향상) + [Header("=== 차징 감속 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 차징 감속 설정 === 을 + [Range(0.1f, 1f)] // 슬라이더를 만들거에요 -> 0.1부터 1 사이의 값으로 조절하는 + [SerializeField] private float minSpeedMultiplier = 0.3f; // 변수를 선언할거에요 -> 차징 중 속도 배율(30%)을 minSpeedMultiplier에 - [Header("=== 충돌 설정 ===")] - [Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")] - [SerializeField] private LayerMask weaponLayer; // 무기를 밟고 올라가는 버그를 막기 위해 무기 레이어를 지정 + [Header("=== 중력 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 중력 설정 === 을 + [SerializeField] private float gravity = -20f; // 변수를 선언할거에요 -> 중력 가속도(-20)를 gravity에 - //[Tooltip("플레이어가 서 있어야 하는 최소 높이 (버그 방지)")] - //[SerializeField] private float minGroundHeight = 0.1f; // 땅 보정 시 바닥에 파묻히지 않게 살짝 띄워주는 값 + [Header("=== 충돌 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 충돌 설정 === 을 + [Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private LayerMask weaponLayer; // 변수를 선언할거에요 -> 충돌을 무시할 무기 레이어를 weaponLayer에 // 내부 상태 변수 (로직 계산용) - private Vector3 _moveDir; // 키보드 입력(WASD)으로 결정된 이동 방향 - private bool _isSprinting; // Shift 키를 눌렀는지 여부 - private bool _isDashing; // 지금 대시 중인가? (중복 대시 방지) - private float _lastDashTime; // 마지막으로 대시를 쓴 시간 (쿨타임 계산용) - private float _verticalVelocity; // 수직 속도 (중력 가속도 계산용) + private Vector3 _moveDir; // 변수를 선언할거에요 -> 입력받은 이동 방향을 저장할 _moveDir을 + private bool _isSprinting; // 변수를 선언할거에요 -> 달리기 중인지 여부를 저장할 _isSprinting을 + private bool _isDashing; // 변수를 선언할거에요 -> 현재 대시 중인지 여부를 저장할 _isDashing을 + private float _lastDashTime; // 변수를 선언할거에요 -> 마지막으로 대시를 쓴 시간을 저장할 _lastDashTime을 + private float _verticalVelocity; // 변수를 선언할거에요 -> 수직(낙하) 속도를 저장할 _verticalVelocity를 // 디버그용 (초기 위치 저장) - private float _initialYPosition; + private float _initialYPosition; // 변수를 선언할거에요 -> 시작 높이를 저장할 _initialYPosition을 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를 { - // 내 몸에 붙어있는 CharacterController를 가져옴 - _controller = GetComponent(); + _controller = GetComponent(); // 컴포넌트를 가져와서 저장할거에요 -> 내 몸에 있는 CharacterController를 _controller에 - // 만약 없다면 콘솔창에 에러를 띄움 - if (_controller == null) + if (_controller == null) // 조건이 맞으면 실행할거에요 -> 컨트롤러를 찾지 못했다면 { - Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); + Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); // 에러 로그를 띄울거에요 -> 컴포넌트가 필요하다는 메시지를 } - _initialYPosition = transform.position.y; + _initialYPosition = transform.position.y; // 값을 저장할거에요 -> 현재의 Y축 위치를 _initialYPosition에 } - private void Start() + private void Start() // 함수를 실행할거에요 -> 첫 프레임 시작 전에 호출되는 Start를 { - // 게임 시작 시 딱 한 번 실행: 무기와 플레이어끼리 충돌 끄기 - SetupLayerCollision(); + SetupLayerCollision(); // 함수를 실행할거에요 -> 레이어 충돌 설정을 담당하는 SetupLayerCollision을 } - /// 레이어 충돌 무시 설정 (플레이어 자살 버그 방지) - - private void SetupLayerCollision() + private void SetupLayerCollision() // 함수를 선언할거에요 -> 레이어 충돌을 설정하는 SetupLayerCollision을 { - int playerLayer = gameObject.layer; // 내 레이어 번호 가져오기 + int playerLayer = gameObject.layer; // 값을 가져올거에요 -> 내 게임 오브젝트의 레이어 번호를 playerLayer에 - // 무기 레이어가 설정되어 있다면 - if (weaponLayer != 0) + if (weaponLayer != 0) // 조건이 맞으면 실행할거에요 -> 무기 레이어가 설정되어 있다면(0이 아니라면) { - // 비트 연산으로 되어있는 LayerMask에서 숫자 인덱스를 뽑음 - int weaponLayerIndex = GetLayerFromMask(weaponLayer); + int weaponLayerIndex = GetLayerFromMask(weaponLayer); // 함수를 실행해서 값을 받을거에요 -> 비트마스크를 정수 인덱스로 변환해서 weaponLayerIndex에 - // 유효한 레이어라면 물리 엔진에게 충돌하지 말라고 명령. - - if (weaponLayerIndex >= 0) + if (weaponLayerIndex >= 0) // 조건이 맞으면 실행할거에요 -> 유효한 레이어 인덱스(0 이상)라면 { - Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true); - Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료"); + Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true); // 설정을 변경할거에요 -> 플레이어와 무기 레이어 간의 충돌을 무시하도록(true) + Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료"); // 로그를 출력할거에요 -> 충돌 무시 설정이 완료되었다는 메시지를 } } } // 비트 마스크(이진수)를 정수 인덱스로 변환하는 수학 함수 - private int GetLayerFromMask(LayerMask mask) + private int GetLayerFromMask(LayerMask mask) // 함수를 선언할거에요 -> 마스크를 인덱스로 바꾸는 GetLayerFromMask를 { - int layerNumber = 0; - int layer = mask.value; - while (layer > 1) + int layerNumber = 0; // 변수를 초기화할거에요 -> 레이어 번호를 셀 layerNumber를 0으로 + int layer = mask.value; // 값을 가져올거에요 -> 마스크의 실제 정수값을 layer에 + while (layer > 1) // 반복할거에요 -> layer 값이 1보다 클 때까지 { - layer = layer >> 1; // 비트를 오른쪽으로 밀면서 횟수를 셉니다. - layerNumber++; + layer = layer >> 1; // 비트 연산을 할거에요 -> 비트를 오른쪽으로 한 칸 밀어서(나누기 2) + layerNumber++; // 값을 증가시킬거에요 -> 레이어 번호 카운트를 1만큼 } - return layerNumber; + return layerNumber; // 값을 반환할거에요 -> 계산된 레이어 번호를 } // 외부(InputHandler)에서 키보드 입력을 넣어주는 함수 - public void SetMoveInput(Vector3 dir, bool sprint) + public void SetMoveInput(Vector3 dir, bool sprint) // 함수를 선언할거에요 -> 이동 입력을 받아오는 SetMoveInput을 { - _moveDir = dir; // 방향 저장 - _isSprinting = sprint; // 달리기 여부 저장 + _moveDir = dir; // 값을 저장할거에요 -> 입력받은 방향(dir)을 _moveDir에 + _isSprinting = sprint; // 값을 저장할거에요 -> 달리기 여부(sprint)를 _isSprinting에 } - public void AttemptDash() + public void AttemptDash() // 함수를 선언할거에요 -> 대시를 시도하는 AttemptDash를 { - // 쿨타임, 생존 여부 등을 체크하고 통과하면 대시 시작 - if (CanDash()) StartCoroutine(DashRoutine()); + if (CanDash()) StartCoroutine(DashRoutine()); // 조건이 맞으면 실행할거에요 -> 대시가 가능하다면(CanDash) 대시 코루틴(DashRoutine)을 } - private bool CanDash() + private bool CanDash() // 함수를 선언할거에요 -> 대시 가능 여부를 판단하는 CanDash를 { - // 현재 시간 > 마지막 대시 시간 + 쿨타임 (즉, 쿨타임 지남) - bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown; - // 체력 스크립트가 있다면 살아있는지 확인 - bool isAlive = health != null && !health.IsDead; - // 쿨타임 끝남 && 대시 중 아님 && 살아있음 -> true - return isCooldownOver && !_isDashing && isAlive; + bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown; // 조건을 검사할거에요 -> 현재 시간이 쿨타임 이후인지 확인해서 isCooldownOver에 + bool isAlive = health != null && !health.IsDead; // 조건을 검사할거에요 -> 체력 스크립트가 있고 살아있는지 확인해서 isAlive에 + return isCooldownOver && !_isDashing && isAlive; // 결과를 반환할거에요 -> 쿨타임 끝남, 대시 안 함, 살아있음이 모두 참일 때만 true를 } - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 { // 1. 행동 불가 상태 체크 (가장 먼저 해서 불필요한 연산 방지) - // 죽었거나, 맞았거나, 대시 중이면 키보드 이동을 막습니다. - if (health != null && (health.IsDead || health.isHit || _isDashing)) + if (health != null && (health.IsDead || health.isHit || _isDashing)) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있고 (죽었거나, 맞았거나, 대시 중이라면) { - if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f); // 애니메이션 멈춤 - ApplyGravityOnly(); // 이동은 못 해도 중력은 받아야 바닥으로 떨어짐 - return; + if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있고 죽지 않았다면 이동 모션을 0(정지)으로 + ApplyGravityOnly(); // 함수를 실행할거에요 -> 이동은 안 해도 중력은 적용하는 ApplyGravityOnly를 + return; // 중단할거에요 -> 더 이상 움직임 코드를 실행하지 않도록 함수를 } // 2. 공격 중 이동 차단 - // 공격 모션 중에 미끄러지듯 이동하는 '스케이트 현상' 방지 - if (attackScript != null && attackScript.IsAttacking) + if (attackScript != null && attackScript.IsAttacking) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있고 공격 중이라면 { - if (pAnim != null) pAnim.UpdateMove(0f); - ApplyGravityOnly(); - return; + if (pAnim != null) pAnim.UpdateMove(0f); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 이동 모션을 0(정지)으로 + ApplyGravityOnly(); // 함수를 실행할거에요 -> 제자리에서 중력만 받는 ApplyGravityOnly를 + return; // 중단할거에요 -> 이동을 막기 위해 함수를 } // 3. 이동 속도 계산 - // Shift 눌렀으면 달리기 속도, 아니면 걷기 속도 - float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; + float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; // 값을 결정할거에요 -> 달리기 중이면 런 스피드를, 아니면 이동 스피드를 speed에 // 공격 차징 중이라면 속도를 느리게 만듦 (긴장감 조성) - if (attackScript != null && attackScript.IsCharging) + if (attackScript != null && attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있고 차징 중이라면 { - // Lerp를 써서 차징 단계에 따라 부드럽게 감속 - float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); - speed *= speedReduction; + float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); // 값을 계산할거에요 -> 차징 진행도에 따라 속도 배율을 줄여서 speedReduction에 + speed *= speedReduction; // 값을 곱할거에요 -> 현재 속도(speed)에 감속 배율을 } // 4. 최종 이동 벡터 계산 - // 방향 * 속도 * 시간(프레임 보정) = 이번 프레임에 움직일 거리 - Vector3 motion = _moveDir * speed * Time.deltaTime; + Vector3 motion = _moveDir * speed * Time.deltaTime; // 벡터를 계산할거에요 -> 방향 * 속도 * 시간을 곱해서 이번 프레임 이동량을 motion에 // 중력 계산 (Y축) - ApplyGravity(); - motion.y = _verticalVelocity * Time.deltaTime; + ApplyGravity(); // 함수를 실행할거에요 -> 수직 속도를 계산하는 ApplyGravity를 + motion.y = _verticalVelocity * Time.deltaTime; // 값을 넣을거에요 -> 수직 이동량(속도 * 시간)을 motion의 y값에 // ⭐ 실제 이동 실행 (여기서 벽 충돌 처리가 자동 수행됨) - _controller.Move(motion); - - // 5. 안전장치 가동 (승천 버그 체크) - //CheckAbnormalHeight(); + _controller.Move(motion); // 이동시킬거에요 -> 캐릭터 컨트롤러를 motion 벡터만큼 // 6. 애니메이션 업데이트 (걷기/뛰기 모션) - UpdateAnimation(); + UpdateAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateAnimation을 } - private void ApplyGravity() + private void ApplyGravity() // 함수를 선언할거에요 -> 중력을 계산하는 ApplyGravity를 { - if (_controller.isGrounded) + if (_controller.isGrounded) // 조건이 맞으면 실행할거에요 -> 캐릭터가 땅에 닿아있다면 { - // 땅에 닿아있어도 -2f 정도로 계속 눌러줘야 경사면에서 붕 뜨지 않음 - _verticalVelocity = -2f; + _verticalVelocity = -2f; // 값을 설정할거에요 -> 바닥에 딱 붙어있도록 약한 하향 속도(-2)를 _verticalVelocity에 } - else + else // 조건이 틀리면 실행할거에요 -> 공중에 떠 있다면 { - // 공중에 있다면 중력 가속도 누적 (점점 빨라짐) - _verticalVelocity += gravity * Time.deltaTime; + _verticalVelocity += gravity * Time.deltaTime; // 값을 더할거에요 -> 중력 가속도를 시간에 맞춰 _verticalVelocity에 } } // 키보드 이동 없이 중력만 적용하는 함수 (피격/공격 중 사용) - private void ApplyGravityOnly() + private void ApplyGravityOnly() // 함수를 선언할거에요 -> 중력만 적용해서 움직이는 ApplyGravityOnly를 { - ApplyGravity(); - _controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0)); - // CheckAbnormalHeight(); // 넉백되다가 날아가지 않게 체크 + ApplyGravity(); // 함수를 실행할거에요 -> 수직 속도를 계산하는 ApplyGravity를 + _controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0)); // 이동시킬거에요 -> 수직 방향으로만 캐릭터를 } - - /// ⭐ 비정상 높이 감지 및 보정 - - //private void CheckAbnormalHeight() - //{ - // // 내 발밑으로 레이저를 쏴서 땅까지의 거리를 잽니다. - // RaycastHit hit; - // if (Physics.Raycast(transform.position, Vector3.down, out hit, 100f)) - // { - // float heightAboveGround = transform.position.y - hit.point.y; - - // // 땅에서 3미터 이상 떠있는데, 시스템상으론 점프 상태가 아니라면? (버그!) - // if (heightAboveGround > 3f && !_controller.isGrounded) - // { - // // 강제로 땅바닥 위치로 좌표를 계산 - // Vector3 correctedPos = transform.position; - // correctedPos.y = hit.point.y + _controller.height / 2f + minGroundHeight; - - // // 순간이동 시킬 땐 CharacterController를 잠시 꺼야 안전함 - // _controller.enabled = false; - // transform.position = correctedPos; - // _controller.enabled = true; - - // _verticalVelocity = 0f; // 낙하 속도 초기화 - - // Debug.LogWarning("[PlayerMovement] 비정상적인 높이 감지! 위치 보정함"); - // } - // } - //} - - private void UpdateAnimation() + private void UpdateAnimation() // 함수를 선언할거에요 -> 애니메이션 파라미터를 조절하는 UpdateAnimation을 { - if (pAnim == null) return; - // 이동 입력이 있으면 1(달리기) 또는 0.5(걷기), 없으면 0(대기) - float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; - // 차징 중이면 무조건 걷기 모션(0.5) 이하로 만듦 - if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; - pAnim.UpdateMove(animVal); + if (pAnim == null) return; // 조건이 맞으면 중단할거에요 -> 애니메이터 스크립트가 없다면 + + float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; // 값을 결정할거에요 -> 움직임이 있으면 (달리기면 1.0, 걷기면 0.5), 없으면 0을 animVal에 + + if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; // 조건이 맞으면 실행할거에요 -> 차징 중이라면 애니메이션 속도를 절반으로 줄여서 + + pAnim.UpdateMove(animVal); // 함수를 실행할거에요 -> 계산된 애니메이션 값(animVal)을 전달하는 UpdateMove를 } // 대시 관련 - private IEnumerator DashRoutine() + private IEnumerator DashRoutine() // 코루틴 함수를 선언할거에요 -> 대시 로직을 수행할 DashRoutine을 { - _isDashing = true; - _lastDashTime = Time.time; + _isDashing = true; // 상태를 바꿀거에요 -> 대시 중 상태를 참(true)으로 + _lastDashTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 대시 시간으로 // 이동 중이면 그 방향으로, 멈춰있으면 뒤로 회피(백스탭) - Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward; + Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward; // 벡터를 결정할거에요 -> 이동 중이면 이동 방향, 아니면 뒤쪽 방향을 dashDir에 // 무적 판정 켜기 (소울류 게임 회피 느낌) - if (health != null) health.isInvincible = true; + if (health != null) health.isInvincible = true; // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 무적 상태를 켜기(true)로 - float startTime = Time.time; - // 정해진 시간(0.08초) 동안 반복 - while (Time.time < startTime + dashDuration) + float startTime = Time.time; // 값을 저장할거에요 -> 대시 시작 시간을 startTime에 + + while (Time.time < startTime + dashDuration) // 반복할거에요 -> 현재 시간이 (시작시간 + 대시지속시간)보다 작을 동안 { - // 속도 = 거리 / 시간 - float speed = dashDistance / dashDuration; - Vector3 dashMotion = dashDir * speed * Time.deltaTime; + float speed = dashDistance / dashDuration; // 값을 계산할거에요 -> 거리 나누기 시간으로 대시 속도를 구해서 speed에 + Vector3 dashMotion = dashDir * speed * Time.deltaTime; // 벡터를 계산할거에요 -> 대시 방향 * 속도 * 시간을 곱해 이동량을 dashMotion에 // ⭐ transform.position += ... 대신 Move()를 쓰는 이유: // Move를 써야 대시 도중 벽을 만나면 뚫지 않고 멈줌 - CollisionFlags flags = _controller.Move(dashMotion); + CollisionFlags flags = _controller.Move(dashMotion); // 이동시킬거에요 -> 캐릭터 컨트롤러를 대시 이동량만큼 - yield return null; // 한 프레임 대기 + yield return null; // 대기할거에요 -> 다음 프레임까지 한 턴을 } // 무적 끄기 및 상태 해제 - if (health != null) health.isInvincible = false; - _isDashing = false; + if (health != null) health.isInvincible = false; // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 무적 상태를 끄기(false)로 + _isDashing = false; // 상태를 바꿀거에요 -> 대시 중 상태를 거짓(false)으로 } // 다른 스크립트에서 상태를 물어볼 때 쓰는 함수들 - public bool IsGrounded() => _controller.isGrounded; - public bool IsDashing() => _isDashing; - public float GetCurrentSpeed() => _controller.velocity.magnitude; + public bool IsGrounded() => _controller.isGrounded; // 값을 반환할거에요 -> 캐릭터가 땅에 닿아있는지 여부를 + public bool IsDashing() => _isDashing; // 값을 반환할거에요 -> 현재 대시 중인지 여부를 + public float GetCurrentSpeed() => _controller.velocity.magnitude; // 값을 반환할거에요 -> 현재 캐릭터의 실제 이동 속도 크기를 } \ No newline at end of file diff --git a/Assets/Scripts/Player/Effect/Player Effect.cs b/Assets/Scripts/Player/Effect/Player Effect.cs index d03d3079..3fa3b11b 100644 --- a/Assets/Scripts/Player/Effect/Player Effect.cs +++ b/Assets/Scripts/Player/Effect/Player Effect.cs @@ -1,41 +1,41 @@ -using UnityEngine; +using UnityEngine; // Ƽ ⺻ ҷðſ -> UnityEngine /// /// ÷̾ ð(VFX) û(SFX) ȿ ϴ ũƮ /// -public class PlayerEffects : MonoBehaviour +public class PlayerEffects : MonoBehaviour // Ŭ Ұſ -> MonoBehaviour ӹ޴ PlayerEffects { - [Header("--- ð ȿ (VFX) ---")] - [SerializeField] private GameObject[] slashEffects; // ޺ ٸ Ʈ - [SerializeField] private Transform slashSpawnPoint; + [Header("--- ð ȿ (VFX) ---")] // ν â ǥҰſ -> --- ð ȿ (VFX) --- + [SerializeField] private GameObject[] slashEffects; // 迭 Ұſ -> Ʈ յ + [SerializeField] private Transform slashSpawnPoint; // Ұſ -> Ʈ ġ - [Header("--- û ȿ (SFX) ---")] - [SerializeField] private AudioClip[] swingSounds; - private AudioSource _audioSource; + [Header("--- û ȿ (SFX) ---")] // ν â ǥҰſ -> --- û ȿ (SFX) --- + [SerializeField] private AudioClip[] swingSounds; // 迭 Ұſ -> ֵθ Ҹ Ŭ + private AudioSource _audioSource; // Ұſ -> ҽ Ʈ - private void Awake() + private void Awake() // Լ Ұſ -> ũƮ Awake { - _audioSource = GetComponent(); - if (_audioSource == null) _audioSource = gameObject.AddComponent(); + _audioSource = GetComponent(); // Ʈ ðſ -> ҽ + if (_audioSource == null) _audioSource = gameObject.AddComponent(); // ߰Ұſ -> ҽ Ʈ } /// /// ִϸ̼ ̺Ʈ ȣ Լ /// /// ޺ ȣ (0~2) - public void PlaySlashEffect(int comboIndex) + public void PlaySlashEffect(int comboIndex) // Լ Ұſ -> Ʈ ϴ PlaySlashEffect { // 1. - if (swingSounds.Length > comboIndex && swingSounds[comboIndex] != null) + if (swingSounds.Length > comboIndex && swingSounds[comboIndex] != null) // Ұſ -> ش ε Ҹ ִٸ { - _audioSource.PlayOneShot(swingSounds[comboIndex]); + _audioSource.PlayOneShot(swingSounds[comboIndex]); // Ұſ -> ش Ŭ } // 2. Ʈ - if (slashEffects.Length > comboIndex && slashEffects[comboIndex] != null && slashSpawnPoint != null) + if (slashEffects.Length > comboIndex && slashEffects[comboIndex] != null && slashSpawnPoint != null) // Ұſ -> ش ε Ʈ ġ ִٸ { - GameObject slash = Instantiate(slashEffects[comboIndex], slashSpawnPoint.position, slashSpawnPoint.rotation); - Destroy(slash, 1.0f); + GameObject slash = Instantiate(slashEffects[comboIndex], slashSpawnPoint.position, slashSpawnPoint.rotation); // Ұſ -> Ʈ Ʈ ġ + Destroy(slash, 1.0f); // ıҰſ -> Ʈ 1 ڿ } } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Equipment/EquipItem.cs b/Assets/Scripts/Player/Equipment/EquipItem.cs index bca24998..328a228c 100644 --- a/Assets/Scripts/Player/Equipment/EquipItem.cs +++ b/Assets/Scripts/Player/Equipment/EquipItem.cs @@ -1,93 +1,101 @@ -using UnityEngine; +using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를 -public class EquippableItem : MonoBehaviour -{ - [SerializeField] private WeaponConfig config; - public WeaponConfig Config => config; +public class EquippableItem : MonoBehaviour // 클래스를 선언할거에요 -> 장착/드랍/투척 가능한 아이템을 처리하는 EquippableItem을 +{ // 코드 블록을 시작할거에요 -> EquippableItem 범위를 - [Header("--- 데미지 밸런스 ---")] - [SerializeField] private float lv1Mult = 1.0f; - [SerializeField] private float lv2Mult = 1.5f; - [SerializeField] private float lv3Mult = 2.5f; - // [제거] strengthBonusFactor 변수 삭제 + [SerializeField] private WeaponConfig config; // 변수를 선언할거에요 -> 이 아이템이 참조할 무기 설정(WeaponConfig)을 config에 + public WeaponConfig Config => config; // 프로퍼티를 선언할거에요 -> 외부에서 config를 읽을 수 있게 Config로 노출 - private Rigidbody _rb; - private Collider _col; - private bool _isThrown; - private int _chargeLevel; - private Stats _thrower; - private Vector3 _originalWorldScale; + [Header("--- 데미지 밸런스 ---")] // 인스펙터에 제목을 표시할거에요 -> 데미지 밸런스 섹션을 + [SerializeField] private float lv1Mult = 1.0f; // 변수를 선언할거에요 -> 차지 1단계 배율을 lv1Mult에 + [SerializeField] private float lv2Mult = 1.5f; // 변수를 선언할거에요 -> 차지 2단계 배율을 lv2Mult에 + [SerializeField] private float lv3Mult = 2.5f; // 변수를 선언할거에요 -> 차지 3단계 배율을 lv3Mult에 + // [제거] strengthBonusFactor 변수 삭제 // 설명을 적을거에요 -> 힘 보너스 배율은 더 이상 사용하지 않음 - private void Awake() - { - _rb = GetComponent(); - _col = GetComponent(); - _originalWorldScale = transform.lossyScale; - } + private Rigidbody _rb; // 변수를 선언할거에요 -> 리지드바디를 저장할 _rb를 + private Collider _col; // 변수를 선언할거에요 -> 콜라이더를 저장할 _col을 + private bool _isThrown; // 변수를 선언할거에요 -> 현재 투척 상태인지 저장할 _isThrown을 + private int _chargeLevel; // 변수를 선언할거에요 -> 투척 당시 차지 레벨을 저장할 _chargeLevel을 + private Stats _thrower; // 변수를 선언할거에요 -> 투척한 주체(플레이어 스탯)를 저장할 _thrower를 + private Vector3 _originalWorldScale; // 변수를 선언할거에요 -> 원래 월드 스케일을 저장할 _originalWorldScale을 - public void OnPickedUp(Transform hand) - { - _isThrown = false; - transform.SetParent(hand); - transform.localScale = new Vector3( - _originalWorldScale.x / hand.lossyScale.x, - _originalWorldScale.y / hand.lossyScale.y, - _originalWorldScale.z / hand.lossyScale.z - ); - transform.localPosition = Vector3.zero; - transform.localRotation = Quaternion.identity; - if (_rb) _rb.isKinematic = true; - if (_col) _col.enabled = false; - } + private void Awake() // 함수를 선언할거에요 -> 오브젝트가 켜질 때 1번 실행되는 Awake를 + { // 코드 블록을 시작할거에요 -> Awake 범위를 + _rb = GetComponent(); // 컴포넌트를 가져올거에요 -> Rigidbody를 찾아 _rb에 저장 + _col = GetComponent(); // 컴포넌트를 가져올거에요 -> Collider를 찾아 _col에 저장 + _originalWorldScale = transform.lossyScale; // 값을 저장할거에요 -> 현재 월드 기준 크기를 원본으로 기억 + } // 코드 블록을 끝낼거에요 -> Awake를 - public void OnDropped(Vector3 throwDir) - { - _isThrown = false; - transform.SetParent(null); - transform.localScale = _originalWorldScale; - if (_rb) - { - _rb.isKinematic = false; - Vector3 force = (throwDir + Vector3.up * 0.5f).normalized * 5f; - _rb.AddForce(force, ForceMode.Impulse); - _rb.AddTorque(Random.insideUnitSphere * 3f, ForceMode.Impulse); - } - if (_col) _col.enabled = true; - } + public void OnPickedUp(Transform hand) // 함수를 선언할거에요 -> 아이템을 손에 집었을 때 처리하는 OnPickedUp을 + { // 코드 블록을 시작할거에요 -> OnPickedUp 범위를 + _isThrown = false; // 상태를 바꿀거에요 -> 투척 상태를 해제(false)로 + transform.SetParent(hand); // 부모를 바꿀거에요 -> 손(Hand) 트랜스폼에 붙이기 - public void OnThrown(Vector3 dir, float force, int lv, Stats s) - { - _isThrown = true; - _chargeLevel = lv; - _thrower = s; - transform.SetParent(null); - transform.localScale = _originalWorldScale; - if (_rb) - { - _rb.isKinematic = false; - _rb.AddForce(dir * force, ForceMode.Impulse); - _rb.AddTorque(transform.right * 10f, ForceMode.Impulse); - } - if (_col) _col.enabled = true; - } + transform.localScale = new Vector3( // 값을 설정할거에요 -> 손의 스케일 변화에도 월드 크기 유지되게 로컬 스케일 계산 + _originalWorldScale.x / hand.lossyScale.x, // x축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로 + _originalWorldScale.y / hand.lossyScale.y, // y축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로 + _originalWorldScale.z / hand.lossyScale.z // z축 크기를 보정할거에요 -> 원본 월드 스케일 기준으로 + ); // 로컬 스케일 설정을 끝낼거에요 -> 월드 크기 유지 - private void OnCollisionEnter(Collision collision) - { - if (!_isThrown) return; - if (_thrower != null && collision.gameObject == _thrower.gameObject) return; + transform.localPosition = Vector3.zero; // 위치를 맞출거에요 -> 손 기준 로컬 위치 0으로 + transform.localRotation = Quaternion.identity; // 회전을 맞출거에요 -> 손 기준 로컬 회전 초기화 + if (_rb) _rb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 리지드바디가 있으면 물리 비활성(키네마틱)으로 + if (_col) _col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있으면 꺼서 손에서 충돌 안 나게 + } // 코드 블록을 끝낼거에요 -> OnPickedUp을 - if (collision.gameObject.TryGetComponent(out var target)) - { - float mult = _chargeLevel == 3 ? lv3Mult : (_chargeLevel == 2 ? lv2Mult : lv1Mult); + public void OnDropped(Vector3 throwDir) // 함수를 선언할거에요 -> 아이템을 떨어뜨릴 때 처리하는 OnDropped를 + { // 코드 블록을 시작할거에요 -> OnDropped 범위를 + _isThrown = false; // 상태를 바꿀거에요 -> 투척 상태가 아님(false)으로 + transform.SetParent(null); // 부모를 해제할거에요 -> 월드에 놓기 + transform.localScale = _originalWorldScale; // 스케일을 되돌릴거에요 -> 원래 월드 스케일로 - // ✨ [수정] 힘 보너스 로직 제거. (플레이어 기본 공격력 + 무기 대미지) * 차지 배율 - float finalDamage = (_thrower.BaseAttackDamage + config.BaseDamage) * mult; + if (_rb) // 조건을 검사할거에요 -> 리지드바디가 있는지 + { // 코드 블록을 시작할거에요 -> 물리 드랍 처리 + _rb.isKinematic = false; // 값을 바꿀거에요 -> 물리 활성화 + Vector3 force = (throwDir + Vector3.up * 0.5f).normalized * 5f; // 힘을 계산할거에요 -> 던지는 방향 + 살짝 위로 + 세기 5 + _rb.AddForce(force, ForceMode.Impulse); // 힘을 줄거에요 -> 순간 힘(Impulse)으로 튕기듯 드랍 + _rb.AddTorque(Random.insideUnitSphere * 3f, ForceMode.Impulse); // 회전도 줄거에요 -> 랜덤 토크로 자연스럽게 굴러가게 + } // 코드 블록을 끝낼거에요 -> 물리 드랍 처리 - target.TakeDamage(finalDamage); - Debug.Log($"[투척 적중] {collision.gameObject.name}에게 {finalDamage:F1} 데미지!"); - _isThrown = false; - } + if (_col) _col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더를 다시 켜기 + } // 코드 블록을 끝낼거에요 -> OnDropped를 - if (collision.gameObject.layer != LayerMask.NameToLayer("Player")) _isThrown = false; - } -} \ No newline at end of file + public void OnThrown(Vector3 dir, float force, int lv, Stats s) // 함수를 선언할거에요 -> 투척 시작 세팅을 하는 OnThrown을 + { // 코드 블록을 시작할거에요 -> OnThrown 범위를 + _isThrown = true; // 상태를 바꿀거에요 -> 투척 상태(true)로 + _chargeLevel = lv; // 값을 저장할거에요 -> 투척 당시 차지 레벨을 + _thrower = s; // 값을 저장할거에요 -> 던진 주체(스탯)를 저장 + transform.SetParent(null); // 부모를 해제할거에요 -> 월드로 분리 + transform.localScale = _originalWorldScale; // 스케일을 되돌릴거에요 -> 원래 월드 스케일로 + + if (_rb) // 조건을 검사할거에요 -> 리지드바디가 있는지 + { // 코드 블록을 시작할거에요 -> 물리 투척 처리 + _rb.isKinematic = false; // 값을 바꿀거에요 -> 물리 활성화 + _rb.AddForce(dir * force, ForceMode.Impulse); // 힘을 줄거에요 -> 던지는 방향 * force로 순간 가속 + _rb.AddTorque(transform.right * 10f, ForceMode.Impulse); // 회전을 줄거에요 -> 오른쪽 축으로 스핀 주기 + } // 코드 블록을 끝낼거에요 -> 물리 투척 처리 + + if (_col) _col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더를 켜서 충돌 가능하게 + } // 코드 블록을 끝낼거에요 -> OnThrown를 + + private void OnCollisionEnter(Collision collision) // 함수를 선언할거에요 -> 충돌이 일어나면 호출되는 OnCollisionEnter를 + { // 코드 블록을 시작할거에요 -> OnCollisionEnter 범위를 + if (!_isThrown) return; // 조건이 맞으면 종료할거에요 -> 투척 상태가 아니면 무시 + if (_thrower != null && collision.gameObject == _thrower.gameObject) return; // 조건이 맞으면 종료할거에요 -> 던진 본인에게는 맞지 않게 + + if (collision.gameObject.TryGetComponent(out var target)) // 조건을 검사할거에요 -> 맞은 대상이 데미지를 받을 수 있는지 + { // 코드 블록을 시작할거에요 -> 데미지 처리 + float mult = _chargeLevel == 3 ? lv3Mult : (_chargeLevel == 2 ? lv2Mult : lv1Mult); // 배율을 고를거에요 -> 차지 레벨에 따라 lv1/2/3 배율 + + // ✨ [수정] 힘 보너스 로직 제거. (플레이어 기본 공격력 + 무기 대미지) * 차지 배율 // 설명을 적을거에요 -> 최종 데미지 계산 공식 + float finalDamage = (_thrower.BaseAttackDamage + config.BaseDamage) * mult; // 값을 계산할거에요 -> (플레이어 공격력 + 무기 기본 데미지) * 배율 + + target.TakeDamage(finalDamage); // 함수를 실행할거에요 -> 대상에게 finalDamage만큼 데미지 주기 + Debug.Log($"[투척 적중] {collision.gameObject.name}에게 {finalDamage:F1} 데미지!"); // 로그를 찍을거에요 -> 적중 결과 출력 + _isThrown = false; // 상태를 바꿀거에요 -> 한 번 맞으면 투척 상태 해제 + } // 코드 블록을 끝낼거에요 -> 데미지 처리 + + if (collision.gameObject.layer != LayerMask.NameToLayer("Player")) _isThrown = false; // 조건이 맞으면 실행할거에요 -> 플레이어 레이어가 아니면 투척 상태 해제 + } // 코드 블록을 끝낼거에요 -> OnCollisionEnter를 + +} // 코드 블록을 끝낼거에요 -> EquippableItem을 \ No newline at end of file diff --git a/Assets/Scripts/Player/Interaction/Heal/HealthAltar.cs b/Assets/Scripts/Player/Interaction/Heal/HealthAltar.cs index cc25ac9d..de67b64b 100644 --- a/Assets/Scripts/Player/Interaction/Heal/HealthAltar.cs +++ b/Assets/Scripts/Player/Interaction/Heal/HealthAltar.cs @@ -1,54 +1,54 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class HealthAltar : MonoBehaviour +public class HealthAltar : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 HealthAltar를 { - [Header("--- 회복 설정 ---")] - [SerializeField] private float healAmount = 50f; // 상호작용 시 즉시 회복량 - [SerializeField] private float interactRange = 3.5f; // 상호작용 가능 거리 + [Header("--- 회복 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 회복 설정 --- 을 + [SerializeField] private float healAmount = 50f; // 변수를 선언할거에요 -> 회복량인 healAmount를 + [SerializeField] private float interactRange = 3.5f; // 변수를 선언할거에요 -> 상호작용 거리인 interactRange를 - [Header("--- 시각 효과 ---")] - [SerializeField] private ParticleSystem healEffect; + [Header("--- 시각 효과 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 시각 효과 --- 를 + [SerializeField] private ParticleSystem healEffect; // 변수를 선언할거에요 -> 회복 이펙트인 healEffect를 // ⭐ [핵심 수정] PlayerInteraction에서 던져주는 'PlayerHealth' 자료형을 직접 받습니다! // - public void Use(PlayerHealth playerHealth) + public void Use(PlayerHealth playerHealth) // 함수를 선언할거에요 -> 제단을 사용하는 Use를 { - if (playerHealth == null) return; + if (playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 체력 스크립트가 없다면 // 플레이어의 Transform 정보 추출 - Transform interactor = playerHealth.transform; + Transform interactor = playerHealth.transform; // 값을 가져올거에요 -> 상호작용하는 대상의 위치 정보를 // 규칙: 실제 몸통 중심점($Center$)과의 거리 계산 - float distance = Vector3.Distance(transform.position, interactor.position); + float distance = Vector3.Distance(transform.position, interactor.position); // 거리를 계산할거에요 -> 제단과 플레이어 사이의 거리를 - if (distance <= interactRange) + if (distance <= interactRange) // 조건이 맞으면 실행할거에요 -> 거리가 상호작용 범위 이내라면 { - ApplyHeal(playerHealth); // 컴포넌트 자체를 넘겨줌 + ApplyHeal(playerHealth); // 함수를 실행할거에요 -> 회복 적용 함수 ApplyHeal을 // 컴포넌트 자체를 넘겨줌 } - else + else // 조건이 틀리면 실행할거에요 -> 거리가 너무 멀다면 { - Debug.Log($"[제단] 너무 멉니다! (거리: {distance:F1} / 제한: {interactRange})"); + Debug.Log($"[제단] 너무 멉니다! (거리: {distance:F1} / 제한: {interactRange})"); // 로그를 출력할거에요 -> 거리 부족 메시지를 } } - private void ApplyHeal(PlayerHealth targetHealth) + private void ApplyHeal(PlayerHealth targetHealth) // 함수를 선언할거에요 -> 실제로 체력을 회복시키는 ApplyHeal을 { // ⭐ 캡슐화 규칙: 플레이어의 Health 스크립트에 있는 기능을 호출합니다. // 유저님의 프로젝트에 정의된 회복 함수 이름을 사용하세요 (예: RestoreHealth, AddHealth 등) - // targetHealth.RestoreHealth(healAmount); // + targetHealth.Heal(healAmount); // 함수를 실행할거에요 -> 플레이어의 회복 함수인 Heal을 // - if (healEffect != null) + if (healEffect != null) // 조건이 맞으면 실행할거에요 -> 이펙트가 설정되어 있다면 { - healEffect.transform.position = targetHealth.transform.position; - healEffect.Play(); + healEffect.transform.position = targetHealth.transform.position; // 위치를 옮길거에요 -> 플레이어 위치로 + healEffect.Play(); // 실행할거에요 -> 이펙트 재생을 } - Debug.Log($"[제단] {targetHealth.gameObject.name} 상호작용 성공! {healAmount} HP 회복."); + Debug.Log($"[제단] {targetHealth.gameObject.name} 상호작용 성공! {healAmount} HP 회복."); // 로그를 출력할거에요 -> 회복 성공 메시지를 } - private void OnDrawGizmosSelected() + private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 선택 시 기즈모를 그리는 OnDrawGizmosSelected를 { - Gizmos.color = Color.cyan; - Gizmos.DrawWireSphere(transform.position, interactRange); + Gizmos.color = Color.cyan; // 색상을 설정할거에요 -> 하늘색으로 + Gizmos.DrawWireSphere(transform.position, interactRange); // 그림을 그릴거에요 -> 상호작용 범위를 표시하는 원을 } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Interaction/PlayerInteraction.cs b/Assets/Scripts/Player/Interaction/PlayerInteraction.cs index 7895295c..afcdae16 100644 --- a/Assets/Scripts/Player/Interaction/PlayerInteraction.cs +++ b/Assets/Scripts/Player/Interaction/PlayerInteraction.cs @@ -1,44 +1,44 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class PlayerInteraction : MonoBehaviour +public class PlayerInteraction : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerInteraction을 { - [Header("--- 설정 ---")] - [SerializeField] private float interactRange = 3f; - [SerializeField] private LayerMask itemLayer; - [SerializeField] private Transform handSlot; - [SerializeField] private Stats playerStats; + [Header("--- 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 설정 --- 을 + [SerializeField] private float interactRange = 3f; // 변수를 선언할거에요 -> 상호작용 거리인 interactRange를 + [SerializeField] private LayerMask itemLayer; // 변수를 선언할거에요 -> 아이템 레이어인 itemLayer를 + [SerializeField] private Transform handSlot; // 변수를 선언할거에요 -> 무기 장착 위치인 handSlot을 + [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트인 playerStats를 - private EquippableItem _currentWeapon; - public EquippableItem CurrentWeapon => _currentWeapon; + private EquippableItem _currentWeapon; // 변수를 선언할거에요 -> 현재 장착된 무기인 _currentWeapon을 + public EquippableItem CurrentWeapon => _currentWeapon; // 프로퍼티를 선언할거에요 -> 현재 무기를 반환하는 CurrentWeapon을 - public void TryInteract() + public void TryInteract() // 함수를 선언할거에요 -> 상호작용을 시도하는 TryInteract를 { - Collider[] hits = Physics.OverlapSphere(transform.position, interactRange, itemLayer); - foreach (var hit in hits) + Collider[] hits = Physics.OverlapSphere(transform.position, interactRange, itemLayer); // 배열을 만들거에요 -> 주변 아이템들을 감지해서 hits에 + foreach (var hit in hits) // 반복할거에요 -> 감지된 모든 아이템에 대해 { // [MODIFIED] ArrowItem 제거 → ArrowPickup으로 통일 - if (hit.TryGetComponent(out var arrowPickup)) + if (hit.TryGetComponent(out var arrowPickup)) // 조건이 맞으면 실행할거에요 -> 대상이 화살 아이템이라면 { - PickupArrow(arrowPickup); - break; + PickupArrow(arrowPickup); // 함수를 실행할거에요 -> 화살 줍기 함수 PickupArrow를 + break; // 중단할거에요 -> 반복문을 (하나만 줍기 위해) } - if (hit.TryGetComponent(out var item)) + if (hit.TryGetComponent(out var item)) // 조건이 맞으면 실행할거에요 -> 대상이 장착 가능한 무기라면 { - EquipWeapon(item); - break; + EquipWeapon(item); // 함수를 실행할거에요 -> 무기 장착 함수 EquipWeapon을 + break; // 중단할거에요 -> 반복문을 } - if (hit.TryGetComponent(out var potion)) + if (hit.TryGetComponent(out var potion)) // 조건이 맞으면 실행할거에요 -> 대상이 회복 물약이라면 { - potion.Use(GetComponent()); - break; + potion.Use(GetComponent()); // 함수를 실행할거에요 -> 물약 사용 함수 Use를 + break; // 중단할거에요 -> 반복문을 } - if (hit.TryGetComponent(out var altar)) + if (hit.TryGetComponent(out var altar)) // 조건이 맞으면 실행할거에요 -> 대상이 회복 제단이라면 { - altar.Use(GetComponent()); - break; + altar.Use(GetComponent()); // 함수를 실행할거에요 -> 제단 사용 함수 Use를 + break; // 중단할거에요 -> 반복문을 } } } @@ -47,47 +47,47 @@ public class PlayerInteraction : MonoBehaviour /// [MODIFIED] ArrowPickup을 직접 처리 /// 기존: ArrowItem 타입 → 변경: ArrowPickup 타입으로 통일 /// - private void PickupArrow(ArrowPickup arrowPickup) + private void PickupArrow(ArrowPickup arrowPickup) // 함수를 선언할거에요 -> 화살을 줍는 PickupArrow를 { - if (arrowPickup == null) return; + if (arrowPickup == null) return; // 조건이 맞으면 중단할거에요 -> 화살 아이템이 없다면 - PlayerAttack playerAttack = GetComponent(); - if (playerAttack != null) + PlayerAttack playerAttack = GetComponent(); // 컴포넌트를 찾을거에요 -> 플레이어 공격 스크립트를 + if (playerAttack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면 { // ArrowPickup.Pickup() 내부에서 // playerAttack.SetCurrentArrow(ArrowData)를 호출합니다 - arrowPickup.Pickup(playerAttack); + arrowPickup.Pickup(playerAttack); // 실행할거에요 -> 화살 아이템의 줍기 함수를 } } - private void EquipWeapon(EquippableItem item) + private void EquipWeapon(EquippableItem item) // 함수를 선언할거에요 -> 무기를 장착하는 EquipWeapon을 { - if (_currentWeapon != null) _currentWeapon.OnDropped(transform.forward); - _currentWeapon = item; - _currentWeapon.OnPickedUp(handSlot); + if (_currentWeapon != null) _currentWeapon.OnDropped(transform.forward); // 조건이 맞으면 실행할거에요 -> 이미 든 무기가 있다면 떨구기를 + _currentWeapon = item; // 값을 저장할거에요 -> 새 무기를 현재 무기로 + _currentWeapon.OnPickedUp(handSlot); // 실행할거에요 -> 무기의 줍기 함수 OnPickedUp을 - if (playerStats != null) + if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 플레이어 스탯이 있다면 { - playerStats.weaponDamage = item.Config.BaseDamage; + playerStats.weaponDamage = item.Config.BaseDamage; // 값을 설정할거에요 -> 무기 공격력을 스탯에 반영하기를 } - FindObjectOfType()?.UpdateStatTexts(); + FindObjectOfType()?.UpdateStatTexts(); // 실행할거에요 -> 스탯 UI 갱신을 } - public void ClearCurrentWeapon() + public void ClearCurrentWeapon() // 함수를 선언할거에요 -> 현재 무기를 해제하는 ClearCurrentWeapon을 { - _currentWeapon = null; - if (playerStats != null) + _currentWeapon = null; // 값을 비울거에요 -> 현재 무기 변수를 + if (playerStats != null) // 조건이 맞으면 실행할거에요 -> 스탯 스크립트가 있다면 { - playerStats.weaponDamage = 0; - FindObjectOfType()?.UpdateStatTexts(); + playerStats.weaponDamage = 0; // 값을 초기화할거에요 -> 무기 공격력을 0으로 + FindObjectOfType()?.UpdateStatTexts(); // 실행할거에요 -> 스탯 UI 갱신을 } // [NEW] 무기 해제 시 화살도 초기화 - PlayerAttack playerAttack = GetComponent(); - if (playerAttack != null) + PlayerAttack playerAttack = GetComponent(); // 컴포넌트를 찾을거에요 -> 공격 스크립트를 + if (playerAttack != null) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있다면 { - playerAttack.ResetArrow(); + playerAttack.ResetArrow(); // 실행할거에요 -> 화살 초기화 함수 ResetArrow를 } } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Stats/PlayerLevelSystem.cs b/Assets/Scripts/Player/Stats/PlayerLevelSystem.cs index b2575d29..634fa299 100644 --- a/Assets/Scripts/Player/Stats/PlayerLevelSystem.cs +++ b/Assets/Scripts/Player/Stats/PlayerLevelSystem.cs @@ -1,70 +1,70 @@ -using System.Collections; -using System.Collections.Generic; -using TMPro; -using UnityEngine; -using UnityEngine.UI; +using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 +using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를 -public class PlayerLevelSystem : MonoBehaviour +public class PlayerLevelSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerLevelSystem을 { - [Header("--- 참조 ---")] - [SerializeField] private Stats stats; - [SerializeField] private PlayerHealth pHealth; + [Header("--- 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 참조 --- 를 + [SerializeField] private Stats stats; // 변수를 선언할거에요 -> 스탯 스크립트인 stats를 + [SerializeField] private PlayerHealth pHealth; // 변수를 선언할거에요 -> 체력 스크립트인 pHealth를 - [Header("레벨 설정")] - public int level = 1; - public int currentExp = 0; - [SerializeField] private int[] expTable; + [Header("레벨 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 레벨 설정 을 + public int level = 1; // 변수를 초기화할거에요 -> 현재 레벨을 1로 + public int currentExp = 0; // 변수를 초기화할거에요 -> 현재 경험치를 0으로 + [SerializeField] private int[] expTable; // 배열을 선언할거에요 -> 레벨별 필요 경험치 테이블인 expTable을 - [Header("UI")] - [SerializeField] private Image expFillImage; - [SerializeField] private TextMeshProUGUI expText; - [SerializeField] private TextMeshProUGUI levelText; + [Header("UI")] // 인스펙터 창에 제목을 표시할거에요 -> UI 를 + [SerializeField] private Image expFillImage; // 변수를 선언할거에요 -> 경험치바 이미지인 expFillImage를 + [SerializeField] private TextMeshProUGUI expText; // 변수를 선언할거에요 -> 경험치 텍스트인 expText를 + [SerializeField] private TextMeshProUGUI levelText; // 변수를 선언할거에요 -> 레벨 텍스트인 levelText를 - public static System.Action OnLevelUp; + public static System.Action OnLevelUp; // 이벤트를 선언할거에요 -> 레벨업 시 호출될 정적 이벤트 OnLevelUp을 - private int RequiredExp + private int RequiredExp // 프로퍼티를 선언할거에요 -> 필요 경험치를 반환하는 RequiredExp를 { get { - int index = level - 1; - if (index >= expTable.Length) return expTable[expTable.Length - 1]; - return expTable[index]; + int index = level - 1; // 인덱스를 계산할거에요 -> 레벨에서 1을 뺀 값으로 + if (index >= expTable.Length) return expTable[expTable.Length - 1]; // 조건이 맞으면 반환할거에요 -> 만렙이면 마지막 필요 경험치를 + return expTable[index]; // 반환할거에요 -> 현재 레벨의 필요 경험치를 } } - private void OnEnable() { MonsterClass.OnMonsterKilled += GainExp; UpdateExpUI(); } - private void OnDisable() { MonsterClass.OnMonsterKilled -= GainExp; } + private void OnEnable() { MonsterClass.OnMonsterKilled += GainExp; UpdateExpUI(); } // 함수를 실행할거에요 -> 몬스터 처치 이벤트 구독과 UI 갱신을 + private void OnDisable() { MonsterClass.OnMonsterKilled -= GainExp; } // 함수를 실행할거에요 -> 몬스터 처치 이벤트 구독 해제를 - void GainExp(int amount) + void GainExp(int amount) // 함수를 선언할거에요 -> 경험치를 획득하는 GainExp를 { - currentExp += amount; - while (currentExp >= RequiredExp) { currentExp -= RequiredExp; LevelUp(); } - UpdateExpUI(); + currentExp += amount; // 값을 더할거에요 -> 획득한 경험치를 현재 경험치에 + while (currentExp >= RequiredExp) { currentExp -= RequiredExp; LevelUp(); } // 반복할거에요 -> 경험치가 꽉 찼다면 레벨업을 + UpdateExpUI(); // 실행할거에요 -> UI 갱신 함수를 } - void LevelUp() + void LevelUp() // 함수를 선언할거에요 -> 레벨업 처리를 하는 LevelUp을 { - if (level >= expTable.Length + 1) { currentExp = 0; return; } - level++; + if (level >= expTable.Length + 1) { currentExp = 0; return; } // 조건이 맞으면 중단할거에요 -> 최대 레벨이라면 경험치만 비우고 리턴 + level++; // 값을 증가시킬거에요 -> 레벨을 1만큼 // ✨ 힘 대신 공격력(+10) 증가 - if (stats != null) stats.AddBaseLevelUpStats(1000f, 10f); + if (stats != null) stats.AddBaseLevelUpStats(1000f, 10f); // 실행할거에요 -> 스탯 증가 함수를 (체력 1000, 공격력 10) - if (pHealth != null) pHealth.RefreshHealthUI(); - StartCoroutine(DelayedCardPopup()); + if (pHealth != null) pHealth.RefreshHealthUI(); // 실행할거에요 -> 체력 UI 갱신을 + StartCoroutine(DelayedCardPopup()); // 코루틴을 시작할거에요 -> 카드 선택 팝업 지연 실행을 } - private IEnumerator DelayedCardPopup() + private IEnumerator DelayedCardPopup() // 코루틴 함수를 선언할거에요 -> 카드 팝업을 지연시키는 DelayedCardPopup을 { - yield return new WaitForSeconds(1.5f); - OnLevelUp?.Invoke(); + yield return new WaitForSeconds(1.5f); // 기다릴거에요 -> 1.5초 동안 + OnLevelUp?.Invoke(); // 이벤트를 실행할거에요 -> 레벨업 알림 이벤트를 } - void UpdateExpUI() + void UpdateExpUI() // 함수를 선언할거에요 -> 경험치 UI를 갱신하는 UpdateExpUI를 { - float fill = (float)currentExp / RequiredExp; - if (expFillImage != null) expFillImage.fillAmount = fill; - if (expText != null) expText.text = $"{currentExp} / {RequiredExp}"; - if (levelText != null) levelText.text = $"Lv. {level}"; + float fill = (float)currentExp / RequiredExp; // 비율을 계산할거에요 -> 현재 경험치 나누기 필요 경험치로 + if (expFillImage != null) expFillImage.fillAmount = fill; // 값을 설정할거에요 -> 게이지 채움 정도를 + if (expText != null) expText.text = $"{currentExp} / {RequiredExp}"; // 텍스트를 바꿀거에요 -> 현재/필요 경험치 수치로 + if (levelText != null) levelText.text = $"Lv. {level}"; // 텍스트를 바꿀거에요 -> 현재 레벨로 } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Stats/StatType.cs b/Assets/Scripts/Player/Stats/StatType.cs index e22f8776..f1a3e12d 100644 --- a/Assets/Scripts/Player/Stats/StatType.cs +++ b/Assets/Scripts/Player/Stats/StatType.cs @@ -1,8 +1,7 @@ - -public enum StatType -{ - Health, - Speed, - // Strength, - Damage -} +public enum StatType // 열거형을 선언할거에요 -> 스탯 종류를 구분하는 StatType을 +{ // 코드 블록을 시작할거에요 -> StatType 범위를 + Health, // 값을 정의할거에요 -> 체력 스탯을 + Speed, // 값을 정의할거에요 -> 이동 속도 스탯을 + // Strength, // 주석 처리할거에요 -> 힘 스탯(현재 미사용) + Damage // 값을 정의할거에요 -> 공격 데미지 스탯을 +} // 코드 블록을 끝낼거에요 -> StatType을 \ No newline at end of file diff --git a/Assets/Scripts/Player/Stats/Stats.cs b/Assets/Scripts/Player/Stats/Stats.cs index c68fee1f..5eab9201 100644 --- a/Assets/Scripts/Player/Stats/Stats.cs +++ b/Assets/Scripts/Player/Stats/Stats.cs @@ -1,56 +1,56 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class Stats : MonoBehaviour +public class Stats : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 Stats를 { - [Header("--- 기본 능력치 ---")] - [SerializeField] private float baseMaxHealth = 100f; - [SerializeField] private float baseMoveSpeed = 5f; - [SerializeField] private float baseAttackDamage = 10f; + [Header("--- 기본 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 기본 능력치 --- 를 + [SerializeField] private float baseMaxHealth = 100f; // 변수를 선언할거에요 -> 기본 최대 체력인 baseMaxHealth를 + [SerializeField] private float baseMoveSpeed = 5f; // 변수를 선언할거에요 -> 기본 이동 속도인 baseMoveSpeed를 + [SerializeField] private float baseAttackDamage = 10f; // 변수를 선언할거에요 -> 기본 공격력인 baseAttackDamage를 - [Header("--- 보너스 능력치 ---")] - public float bonusMaxHealth; - public float bonusMoveSpeed; - public float bonusAttackDamage; + [Header("--- 보너스 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 보너스 능력치 --- 를 + public float bonusMaxHealth; // 변수를 선언할거에요 -> 추가 체력인 bonusMaxHealth를 + public float bonusMoveSpeed; // 변수를 선언할거에요 -> 추가 이동 속도인 bonusMoveSpeed를 + public float bonusAttackDamage; // 변수를 선언할거에요 -> 추가 공격력인 bonusAttackDamage를 - [Header("--- 장착 장비 ---")] - public float weaponDamage; + [Header("--- 장착 장비 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 장착 장비 --- 를 + public float weaponDamage; // 변수를 선언할거에요 -> 무기 데미지인 weaponDamage를 - [Header("--- 밸런스 설정 ---")] - [SerializeField] private float runSpeedMultiplier = 1.5f; + [Header("--- 밸런스 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 밸런스 설정 --- 을 + [SerializeField] private float runSpeedMultiplier = 1.5f; // 변수를 선언할거에요 -> 달리기 속도 배율인 runSpeedMultiplier를 /* ========================= * 실제 게임 로직용 프로퍼티 * ========================= */ - public float MaxHealth => baseMaxHealth + bonusMaxHealth; - public float BaseAttackDamage => baseAttackDamage + bonusAttackDamage; - public float TotalAttackDamage => BaseAttackDamage + weaponDamage; + public float MaxHealth => baseMaxHealth + bonusMaxHealth; // 프로퍼티를 선언할거에요 -> 총 최대 체력을 반환하는 MaxHealth를 + public float BaseAttackDamage => baseAttackDamage + bonusAttackDamage; // 프로퍼티를 선언할거에요 -> 총 기본 공격력을 반환하는 BaseAttackDamage를 + public float TotalAttackDamage => BaseAttackDamage + weaponDamage; // 프로퍼티를 선언할거에요 -> 최종 공격력(무기 포함)을 반환하는 TotalAttackDamage를 // ✨ [수정] 이제 무게 페널티 없이 순수 속도만 계산합니다. - public float CurrentMoveSpeed => baseMoveSpeed + bonusMoveSpeed; - public float CurrentRunSpeed => CurrentMoveSpeed * runSpeedMultiplier; + public float CurrentMoveSpeed => baseMoveSpeed + bonusMoveSpeed; // 프로퍼티를 선언할거에요 -> 현재 이동 속도를 반환하는 CurrentMoveSpeed를 + public float CurrentRunSpeed => CurrentMoveSpeed * runSpeedMultiplier; // 프로퍼티를 선언할거에요 -> 현재 달리기 속도를 반환하는 CurrentRunSpeed를 - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를 { - finalMaxHealth = MaxHealth; - finalMoveSpeed = CurrentMoveSpeed; - finalAttackDamage = TotalAttackDamage; + finalMaxHealth = MaxHealth; // 값을 갱신할거에요 -> 디버그용 최종 체력을 + finalMoveSpeed = CurrentMoveSpeed; // 값을 갱신할거에요 -> 디버그용 최종 속도를 + finalAttackDamage = TotalAttackDamage; // 값을 갱신할거에요 -> 디버그용 최종 공격력을 } // ✨ [수정] 레벨업 시 체력과 공격력을 올려주도록 변경 - public void AddBaseLevelUpStats(float hpAdd, float dmgAdd) + public void AddBaseLevelUpStats(float hpAdd, float dmgAdd) // 함수를 선언할거에요 -> 레벨업 보너스를 적용하는 AddBaseLevelUpStats를 { - baseMaxHealth += hpAdd; - baseAttackDamage += dmgAdd; + baseMaxHealth += hpAdd; // 값을 더할거에요 -> 기본 체력에 증가분을 + baseAttackDamage += dmgAdd; // 값을 더할거에요 -> 기본 공격력에 증가분을 } - public void AddMaxHealth(float value) => bonusMaxHealth += value; - public void AddMoveSpeed(float value) => bonusMoveSpeed += value; - public void AddAttackDamage(float value) => bonusAttackDamage += value; + public void AddMaxHealth(float value) => bonusMaxHealth += value; // 함수를 선언할거에요 -> 추가 체력을 늘리는 AddMaxHealth를 + public void AddMoveSpeed(float value) => bonusMoveSpeed += value; // 함수를 선언할거에요 -> 추가 속도를 늘리는 AddMoveSpeed를 + public void AddAttackDamage(float value) => bonusAttackDamage += value; // 함수를 선언할거에요 -> 추가 공격력을 늘리는 AddAttackDamage를 // ✨ [제거] 무게 및 힘 관련 함수들(UpdateWeaponWeight, ResetWeight)이 삭제되었습니다. - [Header("--- 최종 능력치 (Read Only) ---")] - [SerializeField] private float finalMaxHealth; - [SerializeField] private float finalMoveSpeed; - [SerializeField] private float finalAttackDamage; + [Header("--- 최종 능력치 (Read Only) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최종 능력치 (Read Only) --- 를 + [SerializeField] private float finalMaxHealth; // 변수를 선언할거에요 -> 디버그용 최종 체력 변수를 + [SerializeField] private float finalMoveSpeed; // 변수를 선언할거에요 -> 디버그용 최종 속도 변수를 + [SerializeField] private float finalAttackDamage; // 변수를 선언할거에요 -> 디버그용 최종 공격력 변수를 } \ No newline at end of file diff --git a/Assets/Scripts/Player/Upgrade/Data/CardData.cs b/Assets/Scripts/Player/Upgrade/Data/CardData.cs index a3745453..33faace5 100644 --- a/Assets/Scripts/Player/Upgrade/Data/CardData.cs +++ b/Assets/Scripts/Player/Upgrade/Data/CardData.cs @@ -1,27 +1,27 @@ -using System.Collections; -using System.Collections.Generic; -using TMPro; -using Unity.VisualScripting; -using UnityEngine; +using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을 +using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를 +using Unity.VisualScripting; // 유니티 비주얼 스크립팅 기능을 사용할거에요 -> Unity.VisualScripting을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -[CreateAssetMenu(menuName = "LevelUp/Card")] -public abstract class CardData : ScriptableObject +[CreateAssetMenu(menuName = "LevelUp/Card")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "LevelUp/Card"를 +public abstract class CardData : ScriptableObject // 추상 클래스를 선언할거에요 -> ScriptableObject를 상속받는 CardData를 { - public Sprite icon; + public Sprite icon; // 변수를 선언할거에요 -> 카드 아이콘 이미지를 icon에 // ⭐ 추가: 이 카드가 나타나기 위해 필요한 '복수의 집착' 레벨 // 기본값을 1로 설정하면 처음부터 등장합니다. - public int requiredObsessionLevel = 1; + public int requiredObsessionLevel = 1; // 변수를 선언할거에요 -> 등장 조건 레벨(1)을 requiredObsessionLevel에 // UI 표시용 - public abstract string GetText(); + public abstract string GetText(); // 추상 함수를 선언할거에요 -> 카드 설명 텍스트를 반환하는 GetText를 // 외부에서 호출되는 공통 실행 함수 - public void Execute() + public void Execute() // 함수를 선언할거에요 -> 카드 효과를 실행하는 공통 함수 Execute를 { - ApplyEffect(); + ApplyEffect(); // 함수를 실행할거에요 -> 실제 효과 적용 함수를 } // 실제 효과는 자식이 구현 - protected abstract void ApplyEffect(); -} + protected abstract void ApplyEffect(); // 추상 함수를 선언할거에요 -> 자식 클래스에서 구현할 효과 적용 함수 ApplyEffect를 +} \ No newline at end of file diff --git a/Assets/Scripts/Player/Upgrade/Data/RandomStatCardData.cs b/Assets/Scripts/Player/Upgrade/Data/RandomStatCardData.cs index dae62d45..055e852d 100644 --- a/Assets/Scripts/Player/Upgrade/Data/RandomStatCardData.cs +++ b/Assets/Scripts/Player/Upgrade/Data/RandomStatCardData.cs @@ -1,31 +1,31 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -[CreateAssetMenu(menuName = "LevelUp/RandomStatCard")] -public class RandomStatCardData : CardData +[CreateAssetMenu(menuName = "LevelUp/RandomStatCard")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "LevelUp/RandomStatCard"를 +public class RandomStatCardData : CardData // 클래스를 선언할거에요 -> CardData를 상속받는 RandomStatCardData를 { - public StatType[] possibleStats; - public int minValue = 1; - public int maxValue = 3; + public StatType[] possibleStats; // 배열을 선언할거에요 -> 등장 가능한 스탯 목록을 possibleStats에 + public int minValue = 1; // 변수를 선언할거에요 -> 스탯 증가 최소값을 minValue에 + public int maxValue = 3; // 변수를 선언할거에요 -> 스탯 증가 최대값을 maxValue에 // ⭐ 기존의 stat1, value1 같은 변수들은 다 지워버리세요! // 이제 CardUI가 기억할 거니까 여기엔 필요 없습니다. - public override string GetText() => ""; // CardUI에서 직접 만드니까 비워둡니다. + public override string GetText() => ""; // 함수를 덮어씌울거에요 -> 텍스트 반환 함수를 (CardUI가 처리하므로 빈 문자열 반환) - protected override void ApplyEffect() { } // 사용하지 않음 + protected override void ApplyEffect() { } // 함수를 덮어씌울거에요 -> 기본 효과 적용 함수를 (여기선 안 씀) // ⭐ CardUI가 호출할 수 있게 public으로 만듭니다. - public void ApplyToPlayer(StatType stat, int value) + public void ApplyToPlayer(StatType stat, int value) // 함수를 선언할거에요 -> 플레이어에게 스탯을 적용하는 ApplyToPlayer를 { - Stats stats = FindObjectOfType(); - if (stats == null) return; + Stats stats = FindObjectOfType(); // 컴포넌트를 찾을거에요 -> 씬에 있는 Stats 스크립트를 + if (stats == null) return; // 조건이 맞으면 중단할거에요 -> Stats가 없다면 - switch (stat) + switch (stat) // 분기할거에요 -> 스탯 타입(stat)에 따라 { - case StatType.Health: stats.AddMaxHealth(value); break; - case StatType.Speed: stats.AddMoveSpeed(value); break; - // case StatType.Strength: stats.AddStrength(value); break; - case StatType.Damage: stats.AddAttackDamage(value); break; + case StatType.Health: stats.AddMaxHealth(value); break; // 일치하면 실행할거에요 -> 최대 체력 증가 함수를 + case StatType.Speed: stats.AddMoveSpeed(value); break; // 일치하면 실행할거에요 -> 이동 속도 증가 함수를 + // case StatType.Strength: stats.AddStrength(value); break; // (주석 처리됨) 힘 증가 로직 + case StatType.Damage: stats.AddAttackDamage(value); break; // 일치하면 실행할거에요 -> 공격력 증가 함수를 } } } \ No newline at end of file diff --git a/Assets/Scripts/Player/Upgrade/UI/CardUI.cs b/Assets/Scripts/Player/Upgrade/UI/CardUI.cs index 0f7ac132..a9f3164d 100644 --- a/Assets/Scripts/Player/Upgrade/UI/CardUI.cs +++ b/Assets/Scripts/Player/Upgrade/UI/CardUI.cs @@ -1,59 +1,59 @@ -using UnityEngine; -using TMPro; -using UnityEngine.UI; // +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를 +using UnityEngine.UI; // 유니티 UI 기능을 사용할거에요 -> UnityEngine.UI를 -public class CardUI : MonoBehaviour +public class CardUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 CardUI를 { - [SerializeField] private TextMeshProUGUI effectText; - [SerializeField] private Image iconImage; - [SerializeField] private Outline selectionOutline; // ⭐ 인스펙터에서 테두리 컴포넌트 연결 + [SerializeField] private TextMeshProUGUI effectText; // 변수를 선언할거에요 -> 효과 설명 텍스트 UI를 effectText에 + [SerializeField] private Image iconImage; // 변수를 선언할거에요 -> 아이콘 이미지 UI를 iconImage에 + [SerializeField] private Outline selectionOutline; // 변수를 선언할거에요 -> 선택 시 켜질 테두리 컴포넌트를 selectionOutline에 - private CardData cardData; - private LevelUpUIManager uiManager; + private CardData cardData; // 변수를 선언할거에요 -> 이 UI가 표시할 카드 데이터를 cardData에 + private LevelUpUIManager uiManager; // 변수를 선언할거에요 -> UI 매니저를 uiManager에 - private StatType s1, s2; - private int v1, v2; - private bool isRandomCard = false; + private StatType s1, s2; // 변수를 선언할거에요 -> 랜덤 스탯 타입 2개를 저장할 s1, s2를 + private int v1, v2; // 변수를 선언할거에요 -> 랜덤 스탯 수치 2개를 저장할 v1, v2를 + private bool isRandomCard = false; // 변수를 초기화할거에요 -> 랜덤 카드 여부를 거짓으로 - public void Setup(CardData data, LevelUpUIManager manager) + public void Setup(CardData data, LevelUpUIManager manager) // 함수를 선언할거에요 -> 카드를 설정하는 Setup을 { - cardData = data; - uiManager = manager; - if (selectionOutline != null) selectionOutline.enabled = false; // 처음엔 테두리 끔 + cardData = data; // 값을 저장할거에요 -> 전달받은 카드 데이터를 + uiManager = manager; // 값을 저장할거에요 -> 전달받은 매니저를 + if (selectionOutline != null) selectionOutline.enabled = false; // 조건이 맞으면 실행할거에요 -> 테두리가 있다면 끄기를 // (랜덤 스탯 카드 데이터 처리 로직 - 기존 코드 유지) - RandomStatCardData randomData = cardData as RandomStatCardData; - if (randomData != null) + RandomStatCardData randomData = cardData as RandomStatCardData; // 형변환을 시도할거에요 -> 카드 데이터를 랜덤 스탯 카드 데이터로 + if (randomData != null) // 조건이 맞으면 실행할거에요 -> 랜덤 스탯 카드가 맞다면 { - isRandomCard = true; - s1 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; - do { s2 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; } while (s1 == s2); - v1 = Random.Range(Mathf.Max(1, randomData.minValue), Mathf.Max(1, randomData.maxValue) + 1); - v2 = Random.Range(Mathf.Min(-1, randomData.minValue), -1 + 1); - effectText.text = $"{s1} +{v1}\n{s2} {v2}"; + isRandomCard = true; // 상태를 바꿀거에요 -> 랜덤 카드 상태를 참으로 + s1 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; // 값을 뽑을거에요 -> 첫 번째 스탯을 랜덤으로 + do { s2 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; } while (s1 == s2); // 반복할거에요 -> 두 번째 스탯이 첫 번째와 다를 때까지 뽑기를 + v1 = Random.Range(Mathf.Max(1, randomData.minValue), Mathf.Max(1, randomData.maxValue) + 1); // 값을 뽑을거에요 -> 첫 번째 수치를 범위 내 랜덤으로 + v2 = Random.Range(Mathf.Min(-1, randomData.minValue), -1 + 1); // 값을 뽑을거에요 -> 두 번째 수치를 (음수 범위 등 고려하여) 랜덤으로 + effectText.text = $"{s1} +{v1}\n{s2} {v2}"; // 텍스트를 설정할거에요 -> 뽑힌 스탯과 수치를 UI에 표시하도록 } - else { effectText.text = cardData.GetText(); } + else { effectText.text = cardData.GetText(); } // 조건이 틀리면 실행할거에요 -> 일반 카드라면 기본 텍스트를 표시하도록 } // 카드 클릭 시 매니저에게 알림 - public void OnClick() { uiManager.OnCardClick(this); } + public void OnClick() { uiManager.OnCardClick(this); } // 함수를 실행할거에요 -> 매니저에게 이 카드가 클릭되었음을 알리기를 // 선택 시 하이라이트 연출 - public void SetSelected(bool isSelected) + public void SetSelected(bool isSelected) // 함수를 선언할거에요 -> 선택 상태를 설정하는 SetSelected를 { - if (selectionOutline != null) selectionOutline.enabled = isSelected; - transform.localScale = isSelected ? Vector3.one * 1.05f : Vector3.one; + if (selectionOutline != null) selectionOutline.enabled = isSelected; // 조건이 맞으면 실행할거에요 -> 테두리를 켜거나 끄기를 + transform.localScale = isSelected ? Vector3.one * 1.05f : Vector3.one; // 크기를 조절할거에요 -> 선택되면 살짝 키우고 아니면 원래대로 } // APPLY 버튼 누를 때 최종 실행될 효과 - public void ApplyCurrentEffect() + public void ApplyCurrentEffect() // 함수를 선언할거에요 -> 효과를 최종 적용하는 ApplyCurrentEffect를 { - if (isRandomCard) + if (isRandomCard) // 조건이 맞으면 실행할거에요 -> 랜덤 카드라면 { - RandomStatCardData rd = cardData as RandomStatCardData; - rd.ApplyToPlayer(s1, v1); rd.ApplyToPlayer(s2, v2); + RandomStatCardData rd = cardData as RandomStatCardData; // 형변환할거에요 -> 데이터를 랜덤 카드 데이터로 + rd.ApplyToPlayer(s1, v1); rd.ApplyToPlayer(s2, v2); // 실행할거에요 -> 저장해둔 스탯(s1, s2)과 수치(v1, v2)를 플레이어에게 적용하기를 } - else { cardData.Execute(); } - FindObjectOfType()?.RefreshHealthUI(); + else { cardData.Execute(); } // 조건이 틀리면 실행할거에요 -> 일반 카드라면 기본 실행 함수를 + FindObjectOfType()?.RefreshHealthUI(); // 실행할거에요 -> 체력 UI 갱신을 (혹시 체력이 변했을 수 있으니) } } \ No newline at end of file diff --git a/Assets/Scripts/System_Scripts/SettingPanelController.cs b/Assets/Scripts/System_Scripts/SettingPanelController.cs index ef87eb20..da19db64 100644 --- a/Assets/Scripts/System_Scripts/SettingPanelController.cs +++ b/Assets/Scripts/System_Scripts/SettingPanelController.cs @@ -1,79 +1,79 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; +using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class SettingPanelController : MonoBehaviour +public class SettingPanelController : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 SettingPanelController를 { - [SerializeField] private GameObject settingPanel; - [SerializeField] private GameObject gameStopPanel; - [HideInInspector] public bool IsSettingOpen { get; private set; } + [SerializeField] private GameObject settingPanel; // 변수를 선언할거에요 -> 설정 패널 오브젝트인 settingPanel을 + [SerializeField] private GameObject gameStopPanel; // 변수를 선언할거에요 -> 일시정지 패널 오브젝트인 gameStopPanel을 + [HideInInspector] public bool IsSettingOpen { get; private set; } // 프로퍼티를 선언할거에요 -> 설정창 열림 여부를 나타내는 IsSettingOpen을 (인스펙터에서는 숨김) - [Header("â г")] - [SerializeField] private GameObject normalPanel; - [SerializeField] private GameObject graphicPanel; + [Header("설정창 선택 패널")] // 인스펙터 헤더를 작성할거에요 -> "설정창 선택 패널"이라는 제목을 + [SerializeField] private GameObject normalPanel; // 변수를 선언할거에요 -> 일반 설정 패널인 normalPanel을 + [SerializeField] private GameObject graphicPanel; // 변수를 선언할거에요 -> 그래픽 설정 패널인 graphicPanel을 - [Header(" ݴ Ҹ")] - [SerializeField] private AudioSource audioSource; - [SerializeField] private AudioClip openSound; - [SerializeField] private AudioClip closeSound; + [Header("열고 닫는 소리")] // 인스펙터 헤더를 작성할거에요 -> "열고 닫는 소리"라는 제목을 + [SerializeField] private AudioSource audioSource; // 변수를 선언할거에요 -> 소리를 재생할 오디오 소스 컴포넌트를 + [SerializeField] private AudioClip openSound; // 변수를 선언할거에요 -> 열릴 때 재생할 오디오 클립을 + [SerializeField] private AudioClip closeSound; // 변수를 선언할거에요 -> 닫힐 때 재생할 오디오 클립을 - public void AllClose() + public void AllClose() // 함수를 선언할거에요 -> 모든 하위 패널을 닫는 AllClose를 { - normalPanel.SetActive(false); - graphicPanel.SetActive(false); + normalPanel.SetActive(false); // 기능을 껄거에요 -> 일반 설정 패널을 + graphicPanel.SetActive(false); // 기능을 껄거에요 -> 그래픽 설정 패널을 } - public void NormalOpen() + public void NormalOpen() // 함수를 선언할거에요 -> 일반 설정 패널을 여는 NormalOpen을 { - AllClose(); - normalPanel.SetActive(true); - if (audioSource != null && openSound != null) + AllClose(); // 함수를 실행할거에요 -> 모든 패널을 닫는 기능을 + normalPanel.SetActive(true); // 기능을 켤거에요 -> 일반 설정 패널을 + if (audioSource != null && openSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 열기 사운드가 있다면 { - audioSource.PlayOneShot(openSound); + audioSource.PlayOneShot(openSound); // 소리를 재생할거에요 -> 열기 효과음을 한 번 } } - public void GraphicOpen() + public void GraphicOpen() // 함수를 선언할거에요 -> 그래픽 설정 패널을 여는 GraphicOpen을 { - AllClose(); - graphicPanel.SetActive(true); - if (audioSource != null && openSound != null) + AllClose(); // 함수를 실행할거에요 -> 모든 패널을 닫는 기능을 + graphicPanel.SetActive(true); // 기능을 켤거에요 -> 그래픽 설정 패널을 + if (audioSource != null && openSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 열기 사운드가 있다면 { - audioSource.PlayOneShot(openSound); + audioSource.PlayOneShot(openSound); // 소리를 재생할거에요 -> 열기 효과음을 한 번 } } - public void OpenSetting() + public void OpenSetting() // 함수를 선언할거에요 -> 설정창 전체를 여는 OpenSetting을 { - IsSettingOpen = true; + IsSettingOpen = true; // 상태를 바꿀거에요 -> 설정창 열림 상태를 참(true)으로 - if (audioSource != null && openSound != null) + if (audioSource != null && openSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 열기 사운드가 있다면 { - audioSource.PlayOneShot(openSound); + audioSource.PlayOneShot(openSound); // 소리를 재생할거에요 -> 열기 효과음을 한 번 } - settingPanel.SetActive(true); + settingPanel.SetActive(true); // 기능을 켤거에요 -> 설정 패널 전체를 - if (gameStopPanel != null) + if (gameStopPanel != null) // 조건이 맞으면 실행할거에요 -> 일시정지 패널이 연결되어 있다면 { - gameStopPanel.SetActive(false); + gameStopPanel.SetActive(false); // 기능을 껄거에요 -> 일시정지 패널을 (설정창이랑 겹치지 않게) } } - public void CloseSetting() + public void CloseSetting() // 함수를 선언할거에요 -> 설정창을 닫는 CloseSetting을 { - IsSettingOpen = false; + IsSettingOpen = false; // 상태를 바꿀거에요 -> 설정창 열림 상태를 거짓(false)으로 - if (audioSource != null && closeSound != null) + if (audioSource != null && closeSound != null) // 조건이 맞으면 실행할거에요 -> 오디오 소스와 닫기 사운드가 있다면 { - audioSource.PlayOneShot(closeSound); + audioSource.PlayOneShot(closeSound); // 소리를 재생할거에요 -> 닫기 효과음을 한 번 } - settingPanel.SetActive(false); + settingPanel.SetActive(false); // 기능을 껄거에요 -> 설정 패널 전체를 - if (gameStopPanel != null) + if (gameStopPanel != null) // 조건이 맞으면 실행할거에요 -> 일시정지 패널이 연결되어 있다면 { - gameStopPanel.SetActive(true); + gameStopPanel.SetActive(true); // 기능을 켤거에요 -> 일시정지 패널을 다시 보이게 } } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/ObjectPool/GenericObjectPool.cs b/Assets/Scripts/Systems/ObjectPool/GenericObjectPool.cs index 1d968f28..d35aea38 100644 --- a/Assets/Scripts/Systems/ObjectPool/GenericObjectPool.cs +++ b/Assets/Scripts/Systems/ObjectPool/GenericObjectPool.cs @@ -1,50 +1,50 @@ -using System.Collections.Generic; -using UnityEngine; +using System.Collections.Generic; // 리스트와 딕셔너리 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -public class GenericObjectPool : MonoBehaviour +public class GenericObjectPool : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 GenericObjectPool을 { - public static GenericObjectPool Instance; + public static GenericObjectPool Instance; // 변수를 선언할거에요 -> 싱글톤 인스턴스인 Instance를 - [System.Serializable] - public class Pool + [System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정 가능하게 Pool 클래스를 + public class Pool // 내부 클래스를 선언할거에요 -> 풀 정보를 담을 Pool을 { - public string tag; - public GameObject prefab; - public int size; + public string tag; // 변수를 선언할거에요 -> 풀의 태그 이름을 tag에 + public GameObject prefab; // 변수를 선언할거에요 -> 생성할 프리팹을 prefab에 + public int size; // 변수를 선언할거에요 -> 미리 생성할 개수를 size에 } - public List pools; - public Dictionary> poolDictionary; + public List pools; // 리스트를 선언할거에요 -> 설정된 풀 목록을 pools에 + public Dictionary> poolDictionary; // 딕셔너리를 선언할거에요 -> 태그별 오브젝트 큐를 관리할 poolDictionary를 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를 { - Instance = this; - poolDictionary = new Dictionary>(); + Instance = this; // 값을 저장할거에요 -> 내 자신을 싱글톤 인스턴스에 + poolDictionary = new Dictionary>(); // 초기화할거에요 -> 딕셔너리를 새로 생성해서 - foreach (Pool pool in pools) + foreach (Pool pool in pools) // 반복할거에요 -> 설정된 모든 풀 정보에 대해 { - Queue objectPool = new Queue(); - for (int i = 0; i < pool.size; i++) + Queue objectPool = new Queue(); // 큐를 생성할거에요 -> 오브젝트를 담을 큐를 + for (int i = 0; i < pool.size; i++) // 반복할거에요 -> 설정된 사이즈만큼 { - GameObject obj = Instantiate(pool.prefab); - obj.SetActive(false); // 꺼둔 상태로 보관 - objectPool.Enqueue(obj); + GameObject obj = Instantiate(pool.prefab); // 생성할거에요 -> 프리팹을 인스턴스로 + obj.SetActive(false); // 기능을 껄거에요 -> 생성된 오브젝트를 비활성화 상태로 (보관) + objectPool.Enqueue(obj); // 추가할거에요 -> 큐에 오브젝트를 } - poolDictionary.Add(pool.tag, objectPool); + poolDictionary.Add(pool.tag, objectPool); // 추가할거에요 -> 딕셔너리에 태그와 큐를 } } // ⭐ 몹을 꺼내 쓸 때 호출 (Instantiate 대신 사용) - public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation) + public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation) // 함수를 선언할거에요 -> 풀에서 오브젝트를 꺼내는 SpawnFromPool을 { - if (!poolDictionary.ContainsKey(tag)) return null; + if (!poolDictionary.ContainsKey(tag)) return null; // 조건이 맞으면 반환할거에요 -> 태그가 없다면 null을 - GameObject objectToSpawn = poolDictionary[tag].Dequeue(); - objectToSpawn.SetActive(true); - objectToSpawn.transform.position = position; - objectToSpawn.transform.rotation = rotation; + GameObject objectToSpawn = poolDictionary[tag].Dequeue(); // 꺼낼거에요 -> 큐의 맨 앞 오브젝트를 + objectToSpawn.SetActive(true); // 기능을 켤거에요 -> 오브젝트를 활성화해서 + objectToSpawn.transform.position = position; // 위치를 설정할거에요 -> 지정된 위치로 + objectToSpawn.transform.rotation = rotation; // 회전을 설정할거에요 -> 지정된 회전으로 - poolDictionary[tag].Enqueue(objectToSpawn); // 다시 큐의 끝으로 보냄 - return objectToSpawn; + poolDictionary[tag].Enqueue(objectToSpawn); // 추가할거에요 -> 다시 큐의 맨 뒤로 (재사용 대기) + return objectToSpawn; // 반환할거에요 -> 꺼낸 오브젝트를 } } \ No newline at end of file diff --git a/Assets/Scripts/Systems/Optimization/Editor/PlayerRangeManagerEditor.cs b/Assets/Scripts/Systems/Optimization/Editor/PlayerRangeManagerEditor.cs index fcb9d5ee..a8cc9ec8 100644 --- a/Assets/Scripts/Systems/Optimization/Editor/PlayerRangeManagerEditor.cs +++ b/Assets/Scripts/Systems/Optimization/Editor/PlayerRangeManagerEditor.cs @@ -1,73 +1,73 @@ -using UnityEngine; -using UnityEditor; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEditor; // 유니티 에디터 기능을 사용할거에요 -> UnityEditor를 -namespace GameSystems.Optimization.Editor +namespace GameSystems.Optimization.Editor // 에디터 네임스페이스를 정의할거에요 -> GameSystems.Optimization.Editor로 { - [CustomEditor(typeof(PlayerRangeManager))] - public sealed class PlayerRangeManagerEditor : UnityEditor.Editor + [CustomEditor(typeof(PlayerRangeManager))] // 커스텀 에디터 대상을 지정할거에요 -> PlayerRangeManager 클래스로 + public sealed class PlayerRangeManagerEditor : UnityEditor.Editor // 클래스를 선언할거에요 -> 에디터 상속을 받는 PlayerRangeManagerEditor를 { - private SerializedProperty _configProperty; - private SerializedProperty _parentsProperty; // ⭐ 변수명 일치 + private SerializedProperty _configProperty; // 변수를 선언할거에요 -> 설정 속성을 연결할 _configProperty를 + private SerializedProperty _parentsProperty; // 변수를 선언할거에요 -> 부모 목록 속성을 연결할 _parentsProperty를 - private void OnEnable() + private void OnEnable() // 에디터가 활성화될 때 실행할거에요 -> OnEnable 함수를 { - _configProperty = serializedObject.FindProperty("_config"); + _configProperty = serializedObject.FindProperty("_config"); // 찾을거에요 -> 대상의 "_config" 변수를 // ⭐ [에러 해결] 리스트 이름 '_environmentParents'를 정확히 찾아옵니다. - _parentsProperty = serializedObject.FindProperty("_environmentParents"); + _parentsProperty = serializedObject.FindProperty("_environmentParents"); // 찾을거에요 -> 대상의 "_environmentParents" 변수를 } - public override void OnInspectorGUI() + public override void OnInspectorGUI() // 인스펙터 화면을 그릴거에요 -> OnInspectorGUI 함수를 { - PlayerRangeManager manager = (PlayerRangeManager)target; - serializedObject.Update(); + PlayerRangeManager manager = (PlayerRangeManager)target; // 변수를 설정할거에요 -> 현재 선택된 매니저 객체로 + serializedObject.Update(); // 실행할거에요 -> 객체의 최신 데이터를 불러오기를 // === 헤더 === - DrawMainHeader(); // ⭐ [경고 해결] DrawHeader 이름을 DrawMainHeader로 변경 + DrawMainHeader(); // 함수를 실행할거에요 -> 메인 헤더를 그리는 - EditorGUILayout.Space(5); + EditorGUILayout.Space(5); // 공간을 줄거에요 -> 5픽셀만큼의 여백을 // === 설정 영역 === - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - GUILayout.Label("필수 설정", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(_configProperty); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요 -> 헬프박스 스타일로 + GUILayout.Label("필수 설정", EditorStyles.boldLabel); // 라벨을 표시할거에요 -> "필수 설정"이라는 제목을 + EditorGUILayout.PropertyField(_configProperty); // 그려낼거에요 -> 설정 에셋 입력 필드를 // ⭐ [에러 해결] 리스트를 안전하게 그립니다. - if (_parentsProperty != null) + if (_parentsProperty != null) // 조건이 맞으면 실행할거에요 -> 부모 속성이 유효하다면 { - EditorGUILayout.PropertyField(_parentsProperty, new GUIContent("Environment Parents"), true); + EditorGUILayout.PropertyField(_parentsProperty, new GUIContent("Environment Parents"), true); // 그려낼거에요 -> 자식 요소까지 포함한 리스트 필드를 } - EditorGUILayout.EndVertical(); + EditorGUILayout.EndVertical(); // 수직 영역을 종료할거에요 // === 실시간 통계 === - if (Application.isPlaying && manager.IsInitialized) + if (Application.isPlaying && manager.IsInitialized) // 조건이 맞으면 실행할거에요 -> 게임 중이고 초기화되었다면 { - EditorGUILayout.Space(5); - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - GUILayout.Label("실시간 통계", EditorStyles.boldLabel); - EditorGUILayout.LabelField("총 오브젝트:", manager.TotalObjectCount.ToString()); - EditorGUILayout.LabelField("표시 중:", manager.VisibleObjectCount.ToString()); - EditorGUILayout.LabelField("컬링 효율:", $"{(manager.CullingEfficiency * 100f):F1}%"); - EditorGUILayout.EndVertical(); + EditorGUILayout.Space(5); // 여백을 줄거에요 + EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요 + GUILayout.Label("실시간 통계", EditorStyles.boldLabel); // 제목을 표시할거에요 -> "실시간 통계"를 + EditorGUILayout.LabelField("총 오브젝트:", manager.TotalObjectCount.ToString()); // 텍스트를 표시할거에요 -> 총 개수 정보를 + EditorGUILayout.LabelField("표시 중:", manager.VisibleObjectCount.ToString()); // 텍스트를 표시할거에요 -> 현재 표시 개수 정보를 + EditorGUILayout.LabelField("컬링 효율:", $"{(manager.CullingEfficiency * 100f):F1}%"); // 텍스트를 표시할거에요 -> 효율 퍼센트 정보를 + EditorGUILayout.EndVertical(); // 수직 영역을 종료할거에요 } - EditorGUILayout.Space(5); + EditorGUILayout.Space(5); // 여백을 줄거에요 // === 컨트롤 버튼 === - if (GUILayout.Button("🔄 오브젝트 목록 새로고침", GUILayout.Height(30))) + if (GUILayout.Button("🔄 오브젝트 목록 새로고침", GUILayout.Height(30))) // 조건이 맞으면 실행할거에요 -> 버튼을 눌렀다면 { - if (Application.isPlaying) manager.RefreshObjectList(); + if (Application.isPlaying) manager.RefreshObjectList(); // 실행할거에요 -> 실행 중일 때 목록 갱신 기능을 } - serializedObject.ApplyModifiedProperties(); - if (Application.isPlaying) Repaint(); + serializedObject.ApplyModifiedProperties(); // 실행할거에요 -> 변경된 데이터를 실제 객체에 반영하기를 + if (Application.isPlaying) Repaint(); // 실행할거에요 -> 실행 중일 때 화면을 다시 그리기를 } - private void DrawMainHeader() // ⭐ 이름 변경됨 + private void DrawMainHeader() // 함수를 선언할거에요 -> 메인 헤더를 그리는 DrawMainHeader를 { - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - GUILayout.Label("Player Range Manager", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, alignment = TextAnchor.MiddleCenter }); - EditorGUILayout.EndVertical(); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 수직 영역을 시작할거에요 + GUILayout.Label("Player Range Manager", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, alignment = TextAnchor.MiddleCenter }); // 큰 제목을 그릴거에요 -> 중앙 정렬된 스타일로 + EditorGUILayout.EndVertical(); // 수직 영역을 종료할거에요 } } } \ No newline at end of file diff --git a/Assets/Scripts/Systems/Optimization/Player Render Manager.cs b/Assets/Scripts/Systems/Optimization/Player Render Manager.cs index 4459f9b5..647f3aa9 100644 --- a/Assets/Scripts/Systems/Optimization/Player Render Manager.cs +++ b/Assets/Scripts/Systems/Optimization/Player Render Manager.cs @@ -1,77 +1,68 @@ -using UnityEngine; -using System.Collections.Generic; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 -public class PlayerRangeManager : MonoBehaviour +public class PlayerRangeManager : MonoBehaviour // 클래스를 선언할거에요 -> 거리 기반 최적화 컴포넌트인 PlayerRangeManager를 { - [Header("--- 대상 설정 ---")] - [SerializeField] private Transform environmentParent; // 맵 오브젝트 부모 폴더 + [Header("--- 대상 설정 ---")] // 제목을 표시할거에요 -> --- 대상 설정 --- 을 + [SerializeField] private Transform environmentParent; // 변수를 선언할거에요 -> 맵 오브젝트들이 있는 부모 폴더를 - [Header("--- 최적화 설정 ---")] - [SerializeField] private float renderRange = 40f; // ⭐ 반드시 30~50 사이로 키워주세요! - [SerializeField] private float checkInterval = 0.2f; + [Header("--- 최적화 설정 ---")] // 제목을 표시할거에요 -> --- 최적화 설정 --- 을 + [SerializeField] private float renderRange = 40f; // 변수를 선언할거에요 -> 표시 사거리인 40을 renderRange에 + [SerializeField] private float checkInterval = 0.2f; // 변수를 선언할거에요 -> 검사 주기인 0.2를 checkInterval에 - private struct RenderGroup + private struct RenderGroup // 구조체를 정의할거에요 -> 렌더링 그룹 데이터 구조를 { - public Vector3 actualCenter; // ⭐ 피벗이 아닌 실제 메쉬의 중심점 - public Renderer[] renderers; + public Vector3 actualCenter; // 변수를 선언할거에요 -> 실제 메쉬의 중심 위치를 + public Renderer[] renderers; // 배열을 선언할거에요 -> 포함된 렌더러 목록을 } - private List _childGroups = new List(); - private Transform _player; + private List _childGroups = new List(); // 리스트를 생성할거에요 -> 하위 그룹들을 관리할 목록을 + private Transform _player; // 변수를 선언할거에요 -> 플레이어 위치 정보를 담을 변수를 - private void Start() + private void Start() // 함수를 실행할거에요 -> 시작 시 Start를 { // 태그로 플레이어를 정확히 찾습니다. - GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); - if (playerObj != null) _player = playerObj.transform; - else _player = transform; + GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); // 찾을거에요 -> "Player" 태그를 가진 오브젝트를 + if (playerObj != null) _player = playerObj.transform; // 조건이 맞으면 저장할거에요 -> 플레이어 위치를 + else _player = transform; // 조건이 틀리면 저장할거에요 -> 내 위치를 대신 - if (environmentParent != null) RefreshChildList(); - InvokeRepeating(nameof(UpdateCulling), 0f, checkInterval); + if (environmentParent != null) RefreshChildList(); // 조건이 맞으면 실행할거에요 -> 환경 오브젝트 목록 갱신을 + InvokeRepeating(nameof(UpdateCulling), 0f, checkInterval); // 반복 실행할거에요 -> 컬링 업데이트 함수를 주기적으로 } - public void RefreshChildList() + public void RefreshChildList() // 함수를 선언할거에요 -> 자식 목록을 새로고침하는 RefreshChildList를 { - if (environmentParent == null) return; - _childGroups.Clear(); + if (environmentParent == null) return; // 조건이 맞으면 중단할거에요 -> 부모 폴더가 없다면 + _childGroups.Clear(); // 리스트를 비울거에요 -> 기존 목록을 - foreach (Transform child in environmentParent) + foreach (Transform child in environmentParent) // 반복할거에요 -> 부모 내의 모든 자식 오브젝트에 대해 { - Renderer[] rs = child.GetComponentsInChildren(true); - if (rs.Length > 0) + Renderer[] rs = child.GetComponentsInChildren(true); // 배열을 가져올거에요 -> 자식 내부의 모든 렌더러들을 + if (rs.Length > 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재한다면 { // ⭐ [수정] 피벗(position)이 아니라, 실제 모델링의 중심(bounds.center)을 가져옵니다. // 이래야 "가까이 갔는데 꺼지는" 현상이 완벽히 해결됩니다! - Vector3 center = rs[0].bounds.center; - _childGroups.Add(new RenderGroup { actualCenter = center, renderers = rs }); + Vector3 center = rs[0].bounds.center; // 위치를 가져올거에요 -> 첫 번째 렌더러의 실제 경계 중심점을 + _childGroups.Add(new RenderGroup { actualCenter = center, renderers = rs }); // 리스트에 추가할거에요 -> 새로운 그룹 정보를 생성해서 } } } - private void UpdateCulling() + private void UpdateCulling() // 함수를 선언할거에요 -> 실제 컬링 로직인 UpdateCulling을 { - if (_player == null) return; - Vector3 playerPos = _player.position; + if (_player == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 없다면 + Vector3 playerPos = _player.position; // 값을 가져올거에요 -> 현재 플레이어의 위치를 - foreach (var group in _childGroups) + foreach (var group in _childGroups) // 반복할거에요 -> 모든 관리 그룹에 대해 { // ⭐ [핵심] 실제 몸통 중심점과의 거리를 계산합니다. - float dist = Vector3.Distance(playerPos, group.actualCenter); - bool shouldShow = dist <= renderRange; + float dist = Vector3.Distance(playerPos, group.actualCenter); // 거리를 계산할거에요 -> 플레이어와 그룹 중심 사이의 + bool shouldShow = dist <= renderRange; // 상태를 결정할거에요 -> 범위 이내인지 여부를 - foreach (Renderer r in group.renderers) + foreach (Renderer r in group.renderers) // 반복할거에요 -> 그룹 내 모든 렌더러에 대해 { - if (r != null && r.enabled != shouldShow) r.enabled = shouldShow; + if (r != null && r.enabled != shouldShow) r.enabled = shouldShow; // 조건이 맞으면 상태를 바꿀거에요 -> 렌더러의 활성화 여부를 } } } - - private void OnDrawGizmos() - { - if (_player != null) - { - Gizmos.color = Color.red; - Gizmos.DrawWireSphere(_player.position, renderRange); // 플레이어를 따라다니는 빨간 원 - } - } } \ No newline at end of file diff --git a/Assets/Scripts/Systems/Optimization/PlayerRangeManager.cs b/Assets/Scripts/Systems/Optimization/PlayerRangeManager.cs index 94699790..b9133f5f 100644 --- a/Assets/Scripts/Systems/Optimization/PlayerRangeManager.cs +++ b/Assets/Scripts/Systems/Optimization/PlayerRangeManager.cs @@ -1,151 +1,157 @@ -using UnityEngine; -using System.Collections.Generic; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 -namespace GameSystems.Optimization +namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로 { - [DisallowMultipleComponent] - [AddComponentMenu("Game Systems/Optimization/Player Range Manager")] - public sealed class PlayerRangeManager : MonoBehaviour + [DisallowMultipleComponent] // 기능을 제한할거에요 -> 중복 부착을 방지하도록 + [AddComponentMenu("Game Systems/Optimization/Player Range Manager")] // 메뉴를 추가할거에요 -> 컴포넌트 추가 경로에 + public sealed class PlayerRangeManager : MonoBehaviour // 클래스를 선언할거에요 -> 최적화 관리자인 PlayerRangeManager를 { #region Serialized Fields - [Header("=== 필수 설정 ===")] - [SerializeField, Tooltip("최적화 설정 에셋")] - private RenderOptimizationConfig _config; + [Header("=== 필수 설정 ===")] // 제목을 표시할거에요 -> === 필수 설정 === 을 + [SerializeField, Tooltip("최적화 설정 에셋")] // 필드를 직렬화할거에요 -> 인스펙터 노출을 위해 + private RenderOptimizationConfig _config; // 변수를 선언할거에요 -> 설정 에셋을 담을 _config를 - [SerializeField, Tooltip("환경 오브젝트들이 들어있는 부모 폴더 리스트")] - private List _environmentParents = new List(); // ⭐ 리스트 방식으로 변경 + [SerializeField, Tooltip("환경 오브젝트들이 들어있는 부모 폴더 리스트")] // 툴팁을 추가할거에요 -> 설명 문구를 + private List _environmentParents = new List(); // 리스트를 생성할거에요 -> 부모 폴더들을 관리할 목록을 #endregion #region Private Fields - private Transform _playerTransform; - private readonly List _renderGroups = new List(512); + private Transform _playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 저장할 변수를 + private readonly List _renderGroups = new List(512); // 리스트를 생성할거에요 -> 렌더 그룹들을 담을 목록을 - private bool _isInitialized; - private bool _isPaused; + private bool _isInitialized; // 변수를 선언할거에요 -> 초기화 완료 여부를 + private bool _isPaused; // 변수를 선언할거에요 -> 시스템 일시정지 여부를 - private int _totalObjectCount; - private int _visibleObjectCount; - private int _lastFrameChangedCount; - private float _nextCheckTime; + private int _totalObjectCount; // 변수를 선언할거에요 -> 관리 대상 총 개수를 + private int _visibleObjectCount; // 변수를 선언할거에요 -> 화면에 표시 중인 개수를 + private int _lastFrameChangedCount; // 변수를 선언할거에요 -> 지난 프레임의 상태 변화 수를 + private float _nextCheckTime; // 변수를 선언할거에요 -> 다음 검사 시간을 #endregion #region Public Properties - public bool IsInitialized => _isInitialized; - public bool IsPaused => _isPaused; - public int TotalObjectCount => _totalObjectCount; - public int VisibleObjectCount => _visibleObjectCount; - public float CullingEfficiency => _totalObjectCount > 0 ? 1f - ((float)_visibleObjectCount / _totalObjectCount) : 0f; + public bool IsInitialized => _isInitialized; // 값을 반환할거에요 -> 초기화 완료 여부 상태를 + public bool IsPaused => _isPaused; // 값을 반환할거에요 -> 일시정지 여부 상태를 + public int TotalObjectCount => _totalObjectCount; // 값을 반환할거에요 -> 총 관리 개수 수치를 + public int VisibleObjectCount => _visibleObjectCount; // 값을 반환할거에요 -> 현재 표시 개수 수치를 + public float CullingEfficiency => _totalObjectCount > 0 ? 1f - ((float)_visibleObjectCount / _totalObjectCount) : 0f; // 값을 계산해서 반환할거에요 -> 컬링 효율 비율을 #endregion - private void Awake() { ValidateConfiguration(); } - private void Start() { InitializeSystem(); } + private void Awake() { ValidateConfiguration(); } // 함수를 실행할거에요 -> 시작 시 설정을 검증하는 Awake를 + private void Start() { InitializeSystem(); } // 함수를 실행할거에요 -> 시스템을 초기화하는 Start를 - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트 로직인 Update를 { - if (!_isInitialized || _isPaused) return; - if (Time.time >= _nextCheckTime) + if (!_isInitialized || _isPaused || _playerTransform == null) return; // 조건이 맞으면 중단할거에요 -> 동작 불가능한 상태라면 + + if (Time.time >= _nextCheckTime) // 조건이 맞으면 실행할거에요 -> 다음 검사 시간이 되었다면 { - UpdateCulling(); - _nextCheckTime = Time.time + GetCheckInterval(); + UpdateCulling(); // 함수를 실행할거에요 -> 컬링 업데이트 기능을 + _nextCheckTime = Time.time + GetCheckInterval(); // 값을 갱신할거에요 -> 다음 검사 예정 시간을 } } - private void ValidateConfiguration() + private void ValidateConfiguration() // 함수를 선언할거에요 -> 설정 누락 여부를 확인하는 ValidateConfiguration을 { - if (_config == null) _config = RenderOptimizationConfig.CreateDefault(); - } - - public void InitializeSystem() - { - if (!FindPlayer()) return; - if (!ScanEnvironmentObjects()) return; - - _isInitialized = true; - _nextCheckTime = Time.time; - } - - private bool FindPlayer() - { - GameObject playerObj = GameObject.FindGameObjectWithTag(GetPlayerTag()); - _playerTransform = (playerObj != null) ? playerObj.transform : transform; - return true; - } - - // ⭐ [핵심 수정] 리스트 내의 모든 폴더를 뒤져서 '낱개' 단위로 등록합니다. - private bool ScanEnvironmentObjects() - { - if (_environmentParents == null || _environmentParents.Count == 0) return false; - - _renderGroups.Clear(); - - foreach (Transform parent in _environmentParents) + if (_config == null) // 조건이 맞으면 실행할거에요 -> 설정 에셋이 비어있다면 { - if (parent == null) continue; + Debug.LogWarning("[PlayerRangeManager] Config가 없습니다! 기본값을 생성합니다."); // 경고를 출력할거에요 -> 자동 생성 메시지를 + _config = RenderOptimizationConfig.CreateDefault(); // 값을 설정할거에요 -> 기본값으로 생성된 에셋을 + } + } - // ⭐ 폴더 구조가 몇 층이든 상관없이 모든 Renderer를 낱낱이 찾아냅니다. - Renderer[] allRenderers = parent.GetComponentsInChildren(_config.IncludeInactiveObjects); + private void InitializeSystem() // 함수를 선언할거에요 -> 전체 시스템을 초기화하는 InitializeSystem을 + { + FindPlayer(); // 함수를 실행할거에요 -> 플레이어를 찾는 기능을 + ScanEnvironmentObjects(); // 함수를 실행할거에요 -> 주변 오브젝트를 스캔하는 기능을 - foreach (Renderer r in allRenderers) + _isInitialized = true; // 상태를 바꿀거에요 -> 초기화 완료인 참(true)으로 + if (ShouldLog()) Debug.Log($"[PlayerRangeManager] 초기화 완료. 대상 그룹: {_totalObjectCount}"); // 조건이 맞으면 로그를 출력할거에요 -> 결과 보고 메시지를 + } + + private void FindPlayer() // 함수를 선언할거에요 -> 플레이어를 탐색하는 FindPlayer를 + { + GameObject player = GameObject.FindGameObjectWithTag(GetPlayerTag()); // 오브젝트를 찾을거에요 -> 지정된 태그를 가진 플레이어를 + if (player != null) // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면 + { + _playerTransform = player.transform; // 값을 저장할거에요 -> 플레이어의 Transform 정보를 + } + else // 조건이 틀리면 실행할거에요 -> 플레이어를 못 찾았다면 + { + Debug.LogError($"[PlayerRangeManager] '{GetPlayerTag()}' 태그의 플레이어를 찾을 수 없습니다!"); // 에러를 출력할거에요 -> 태그 불일치 메시지를 + } + } + + private void ScanEnvironmentObjects() // 함수를 선언할거에요 -> 환경 오브젝트들을 스캔하는 ScanEnvironmentObjects를 + { + _renderGroups.Clear(); // 리스트를 비울거에요 -> 기존에 있던 렌더 그룹들을 + + foreach (var parent in _environmentParents) // 반복할거에요 -> 등록된 모든 부모 폴더에 대해 + { + if (parent == null) continue; // 조건이 맞으면 건너뛸거에요 -> 부모가 비어있다면 + + foreach (Transform child in parent) // 반복할거에요 -> 부모 폴더의 모든 자식 오브젝트에 대해 { - if (r == null) continue; - // 각 렌더러를 독립적인 그룹으로 생성 (개별 최적화의 핵심) - _renderGroups.Add(new RenderGroup(new Renderer[] { r })); + Renderer[] renderers = child.GetComponentsInChildren(true); // 배열을 가져올거에요 -> 자식 내부의 모든 렌더러들을 + if (renderers.Length > 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재한다면 + { + _renderGroups.Add(new RenderGroup(renderers)); // 리스트에 추가할거에요 -> 새로운 렌더 그룹을 생성해서 + } } } - _totalObjectCount = _renderGroups.Count; - return _totalObjectCount > 0; + _totalObjectCount = _renderGroups.Count; // 값을 저장할거에요 -> 총 관리 대상 개수를 } - private void UpdateCulling() + private void UpdateCulling() // 함수를 선언할거에요 -> 컬링을 업데이트하는 UpdateCulling을 { - if (_playerTransform == null) return; + Vector3 playerPos = _playerTransform.position; // 값을 가져올거에요 -> 현재 플레이어의 위치를 + float range = GetRenderRange(); // 값을 가져올거에요 -> 설정된 렌더링 거리 수치를 - Vector3 playerPosition = _playerTransform.position; - float maxDistance = GetRenderRange(); - int changedCount = 0; - int visibleCount = 0; + int changedCount = 0; // 변수를 초기화할거에요 -> 변경 개수를 0으로 + int visibleCount = 0; // 변수를 초기화할거에요 -> 표시 중인 개수를 0으로 - for (int i = 0; i < _renderGroups.Count; i++) + for (int i = 0; i < _renderGroups.Count; i++) // 반복할거에요 -> 모든 렌더 그룹에 대해 { - if (_renderGroups[i].UpdateVisibility(playerPosition, maxDistance)) changedCount++; - if (_renderGroups[i].IsVisible) visibleCount++; + if (_renderGroups[i].UpdateVisibility(playerPos, range)) changedCount++; // 조건이 맞으면 증가시킬거에요 -> 상태가 바뀌었다면 변경 수를 + if (_renderGroups[i].IsVisible) visibleCount++; // 조건이 맞으면 증가시킬거에요 -> 현재 보이고 있다면 표시 수를 } - _visibleObjectCount = visibleCount; - _lastFrameChangedCount = changedCount; + _visibleObjectCount = visibleCount; // 값을 저장할거에요 -> 이번 검사 결과의 총 표시 개수를 + _lastFrameChangedCount = changedCount; // 값을 저장할거에요 -> 변화가 발생한 그룹 수를 } - public void RefreshObjectList() { ScanEnvironmentObjects(); } - public void ShowAllObjects() { foreach (var g in _renderGroups) g.ForceShow(); _visibleObjectCount = _totalObjectCount; } - public void HideAllObjects() { foreach (var g in _renderGroups) g.ForceHide(); _visibleObjectCount = 0; } - public void SetPaused(bool paused) => _isPaused = paused; + public void RefreshObjectList() { ScanEnvironmentObjects(); } // 함수를 실행할거에요 -> 목록을 새로 스캔하는 기능을 + public void ShowAllObjects() { foreach (var g in _renderGroups) g.ForceShow(); _visibleObjectCount = _totalObjectCount; } // 함수를 실행할거에요 -> 모든 대상을 켜는 기능을 + public void HideAllObjects() { foreach (var g in _renderGroups) g.ForceHide(); _visibleObjectCount = 0; } // 함수를 실행할거에요 -> 모든 대상을 끄는 기능을 + public void SetPaused(bool paused) => _isPaused = paused; // 값을 바꿀거에요 -> 일시정지 여부를 전달받은 인자대로 - private float GetRenderRange() => _config.RenderRange; - private float GetCheckInterval() => _config.CheckInterval; - private string GetPlayerTag() => _config.PlayerTag; - private bool ShouldShowGizmos() => _config.ShowGizmos; + private float GetRenderRange() => _config.RenderRange; // 값을 가져올거에요 -> 설정 파일의 렌더링 사거리 수치를 + private float GetCheckInterval() => _config.CheckInterval; // 값을 가져올거에요 -> 설정 파일의 검사 간격 수치를 + private string GetPlayerTag() => _config.PlayerTag; // 값을 가져올거에요 -> 설정 파일의 플레이어 태그를 + private bool ShouldShowGizmos() => _config.ShowGizmos; // 값을 가져올거에요 -> 설정 파일의 기즈모 표시 여부를 + private bool ShouldLog() => _config.EnableVerboseLogging; // 값을 가져올거에요 -> 설정 파일의 로그 활성화 여부를 - private void OnDrawGizmos() + private void OnDrawGizmos() // 함수를 실행할거에요 -> 기즈모를 그리는 OnDrawGizmos를 { - if (!ShouldShowGizmos() || _playerTransform == null) return; - Gizmos.color = new Color(1f, 0f, 0f, 0.3f); - Gizmos.DrawWireSphere(_playerTransform.position, GetRenderRange()); + if (!ShouldShowGizmos() || _playerTransform == null) return; // 조건이 맞으면 중단할거에요 -> 표시 설정이 꺼졌거나 플레이어가 없다면 + Gizmos.color = new Color(1f, 0f, 0f, 0.3f); // 색상을 설정할거에요 -> 빨간색 반투명으로 + Gizmos.DrawWireSphere(_playerTransform.position, GetRenderRange()); // 그림을 그릴거에요 -> 플레이어 중심의 사거리 구체를 } - private void OnDrawGizmosSelected() + private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 선택 시 기즈모를 그리는 OnDrawGizmosSelected를 { - if (!ShouldShowGizmos() || !_isInitialized) return; - foreach (var group in _renderGroups) + if (!ShouldShowGizmos() || !_isInitialized) return; // 조건이 맞으면 중단할거에요 -> 표시 설정이 꺼졌거나 초기화 전이라면 + foreach (var group in _renderGroups) // 반복할거에요 -> 모든 렌더 그룹에 대해 { - Gizmos.color = group.IsVisible ? new Color(0f, 1f, 0f, 0.5f) : new Color(0.5f, 0.5f, 0.5f, 0.2f); - Gizmos.DrawSphere(group.ActualCenter, 0.3f); + Gizmos.color = group.IsVisible ? new Color(0f, 1f, 0f, 0.5f) : new Color(0.5f, 0.5f, 0.5f, 0.2f); // 색상을 결정할거에요 -> 상태에 따라 초록 혹은 회색으로 + Gizmos.DrawWireSphere(group.ActualCenter, group.BoundingRadius); // 그림을 그릴거에요 -> 각 그룹의 경계면 구체를 } } } diff --git a/Assets/Scripts/Systems/Optimization/RenderGroup.cs b/Assets/Scripts/Systems/Optimization/RenderGroup.cs index 993bdffa..a686a9f6 100644 --- a/Assets/Scripts/Systems/Optimization/RenderGroup.cs +++ b/Assets/Scripts/Systems/Optimization/RenderGroup.cs @@ -1,21 +1,21 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -namespace GameSystems.Optimization +namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로 { /// /// 렌더링 최적화를 위한 오브젝트 그룹 데이터 /// - 불변성(Immutability) 보장 /// - 캡슐화된 내부 데이터 /// - public sealed class RenderGroup + public sealed class RenderGroup // 클래스를 선언할거에요 -> 상속 불가능한 최적화 그룹인 RenderGroup을 { #region Private Fields - - private readonly Vector3 _actualCenter; - private readonly Renderer[] _renderers; - private readonly float _boundingRadius; - private bool _isCurrentlyVisible; - + + private readonly Vector3 _actualCenter; // 변수를 선언할거에요 -> 읽기 전용으로 실제 중심 좌표를 + private readonly Renderer[] _renderers; // 배열을 선언할거에요 -> 읽기 전용으로 렌더러 목록을 + private readonly float _boundingRadius; // 변수를 선언할거에요 -> 읽기 전용으로 경계 반지름 수치를 + private bool _isCurrentlyVisible; // 변수를 선언할거에요 -> 현재 표시 상태를 저장할 변수를 + #endregion #region Public Properties (Read-Only) @@ -23,22 +23,22 @@ namespace GameSystems.Optimization /// /// 실제 메쉬의 중심점 (피벗이 아닌 bounds 기준) /// - public Vector3 ActualCenter => _actualCenter; + public Vector3 ActualCenter => _actualCenter; // 프로퍼티를 선언할거에요 -> 실제 중심점을 반환하는 기능을 /// /// 오브젝트의 최대 반지름 (경계 구체) /// - public float BoundingRadius => _boundingRadius; + public float BoundingRadius => _boundingRadius; // 프로퍼티를 선언할거에요 -> 경계 반지름을 반환하는 기능을 /// /// 현재 렌더링 상태 /// - public bool IsVisible => _isCurrentlyVisible; + public bool IsVisible => _isCurrentlyVisible; // 프로퍼티를 선언할거에요 -> 표시 여부 상태를 반환하는 기능을 /// /// 관리 중인 Renderer 개수 /// - public int RendererCount => _renderers?.Length ?? 0; + public int RendererCount => _renderers?.Length ?? 0; // 프로퍼티를 선언할거에요 -> 렌더러 개수를 반환하는 기능을 (null이면 0) #endregion @@ -48,28 +48,28 @@ namespace GameSystems.Optimization /// RenderGroup 생성자 /// /// 대상 Renderer 배열 (null 체크 수행) - public RenderGroup(Renderer[] renderers) + public RenderGroup(Renderer[] renderers) // 생성자를 실행할거에요 -> 렌더러 배열을 인자로 받아서 { - if (renderers == null || renderers.Length == 0) + if (renderers == null || renderers.Length == 0) // 조건이 맞으면 실행할거에요 -> 렌더러가 비어있거나 없다면 { - Debug.LogError("[RenderGroup] Renderer 배열이 null 또는 비어있습니다."); - _renderers = System.Array.Empty(); - _actualCenter = Vector3.zero; - _boundingRadius = 0f; - return; + Debug.LogError("[RenderGroup] Renderer 배열이 null 또는 비어있습니다."); // 에러를 출력할거에요 -> 로그 메시지를 + _renderers = System.Array.Empty(); // 값을 할당할거에요 -> 빈 렌더러 배열을 + _actualCenter = Vector3.zero; // 값을 할당할거에요 -> 제로 벡터를 중심점으로 + _boundingRadius = 0f; // 값을 할당할거에요 -> 0을 반지름으로 + return; // 중단할거에요 -> 초기화 로직을 } // 방어적 복사 (외부에서 배열 수정 방지) - _renderers = new Renderer[renderers.Length]; - System.Array.Copy(renderers, _renderers, renderers.Length); + _renderers = new Renderer[renderers.Length]; // 배열을 생성할거에요 -> 같은 크기의 새로운 렌더러 배열을 + System.Array.Copy(renderers, _renderers, renderers.Length); // 값을 복사할거에요 -> 원본 렌더러들을 내부 배열로 // 실제 중심점 계산 - _actualCenter = CalculateGroupCenter(_renderers); - + _actualCenter = CalculateGroupCenter(_renderers); // 값을 계산해서 넣을거에요 -> 그룹의 통합 중심점을 + // 경계 반지름 계산 - _boundingRadius = CalculateBoundingRadius(_renderers, _actualCenter); - - _isCurrentlyVisible = true; // 초기 상태는 표시 + _boundingRadius = CalculateBoundingRadius(_renderers, _actualCenter); // 값을 계산해서 넣을거에요 -> 최대 경계 반지름을 + + _isCurrentlyVisible = true; // 초기값을 설정할거에요 -> 표시 상태를 참(true)으로 } #endregion @@ -82,58 +82,58 @@ namespace GameSystems.Optimization /// 기준 위치 (플레이어 등) /// 최대 렌더링 거리 /// 상태가 변경되었는지 여부 - public bool UpdateVisibility(Vector3 referencePosition, float maxDistance) + public bool UpdateVisibility(Vector3 referencePosition, float maxDistance) // 함수를 선언할거에요 -> 거리에 따른 표시 갱신 기능을 { // 거리 계산 (오브젝트 크기 고려) - float distance = Vector3.Distance(referencePosition, _actualCenter); - bool shouldBeVisible = (distance - _boundingRadius) <= maxDistance; + float distance = Vector3.Distance(referencePosition, _actualCenter); // 거리를 계산할거에요 -> 기준점과 그룹 중심 사이의 + bool shouldBeVisible = (distance - _boundingRadius) <= maxDistance; // 상태를 판단할거에요 -> 사거리와 반지름을 고려해서 표시 여부를 // 상태 변경이 필요한 경우에만 처리 - if (shouldBeVisible == _isCurrentlyVisible) + if (shouldBeVisible == _isCurrentlyVisible) // 조건이 맞으면 실행할거에요 -> 이전 상태와 동일하다면 { - return false; // 변경 없음 + return false; // 반환할거에요 -> 변경되지 않았음을 } // Renderer 상태 일괄 변경 - SetRenderersState(shouldBeVisible); - _isCurrentlyVisible = shouldBeVisible; - - return true; // 변경됨 + SetRenderersState(shouldBeVisible); // 함수를 실행할거에요 -> 모든 렌더러의 활성화 상태를 바꾸는 기능을 + _isCurrentlyVisible = shouldBeVisible; // 값을 갱신할거에요 -> 현재 표시 상태를 + + return true; // 반환할거에요 -> 상태가 변경되었음을 } /// /// 강제로 모든 Renderer 표시 /// - public void ForceShow() + public void ForceShow() // 함수를 선언할거에요 -> 강제 표시 기능인 ForceShow를 { - SetRenderersState(true); - _isCurrentlyVisible = true; + SetRenderersState(true); // 함수를 실행할거에요 -> 모든 렌더러를 켜는 기능을 + _isCurrentlyVisible = true; // 상태를 설정할거에요 -> 표시 중인 것으로 } /// /// 강제로 모든 Renderer 숨김 /// - public void ForceHide() + public void ForceHide() // 함수를 선언할거에요 -> 강제 숨김 기능인 ForceHide를 { - SetRenderersState(false); - _isCurrentlyVisible = false; + SetRenderersState(false); // 함수를 실행할거에요 -> 모든 렌더러를 끄는 기능을 + _isCurrentlyVisible = false; // 상태를 설정할거에요 -> 숨김 중인 것으로 } /// /// 그룹 유효성 검사 (Renderer가 파괴되었는지 확인) /// - public bool IsValid() + public bool IsValid() // 함수를 선언할거에요 -> 유효성 검사 기능인 IsValid를 { - if (_renderers == null || _renderers.Length == 0) - return false; + if (_renderers == null || _renderers.Length == 0) // 조건이 맞으면 반환할거에요 -> 배열이 비어있다면 거짓(false)을 + return false; // 반환할거에요 -> 유효하지 않음을 // 하나라도 유효한 Renderer가 있으면 true - foreach (Renderer r in _renderers) + foreach (Renderer r in _renderers) // 반복할거에요 -> 배열 내 모든 렌더러에 대해 { - if (r != null) return true; + if (r != null) return true; // 조건이 맞으면 반환할거에요 -> 살아있는 객체가 있다면 참(true)을 } - return false; + return false; // 반환할거에요 -> 모두 파괴되었다면 거짓을 } #endregion @@ -143,73 +143,73 @@ namespace GameSystems.Optimization /// /// 여러 Renderer의 통합 중심점 계산 /// - private static Vector3 CalculateGroupCenter(Renderer[] renderers) + private static Vector3 CalculateGroupCenter(Renderer[] renderers) // 함수를 선언할거에요 -> 중심점을 계산하는 정적 기능을 { - if (renderers.Length == 1 && renderers[0] != null) + if (renderers.Length == 1 && renderers[0] != null) // 조건이 맞으면 실행할거에요 -> 렌더러가 하나뿐이라면 { - return renderers[0].bounds.center; + return renderers[0].bounds.center; // 반환할거에요 -> 그 렌더러의 중심점을 } // 모든 bounds를 포함하는 통합 경계 계산 - Bounds? combinedBounds = null; + Bounds? combinedBounds = null; // 변수를 선언할거에요 -> 널 허용 경계값 변수를 - foreach (Renderer r in renderers) + foreach (Renderer r in renderers) // 반복할거에요 -> 모든 렌더러에 대해 { - if (r == null) continue; + if (r == null) continue; // 조건이 맞으면 건너뛸거에요 -> 렌더러가 비어있다면 - if (!combinedBounds.HasValue) + if (!combinedBounds.HasValue) // 조건이 맞으면 실행할거에요 -> 첫 번째 유효한 경계값이라면 { - combinedBounds = r.bounds; + combinedBounds = r.bounds; // 값을 할당할거에요 -> 현재 렌더러의 경계값으로 } - else + else // 조건이 틀리면 실행할거에요 -> 이미 경계값이 존재한다면 { - Bounds temp = combinedBounds.Value; - temp.Encapsulate(r.bounds); - combinedBounds = temp; + Bounds temp = combinedBounds.Value; // 변수를 설정할거에요 -> 임시 경계값으로 + temp.Encapsulate(r.bounds); // 영역을 확장할거에요 -> 새로운 렌더러를 포함하도록 + combinedBounds = temp; // 값을 갱신할거에요 -> 확장된 경계값으로 } } - return combinedBounds?.center ?? Vector3.zero; + return combinedBounds?.center ?? Vector3.zero; // 반환할거에요 -> 계산된 중심점을 (없으면 제로 벡터) } /// /// 오브젝트의 최대 반지름 계산 /// - private static float CalculateBoundingRadius(Renderer[] renderers, Vector3 center) + private static float CalculateBoundingRadius(Renderer[] renderers, Vector3 center) // 함수를 선언할거에요 -> 반지름을 계산하는 정적 기능을 { - float maxRadius = 0f; + float maxRadius = 0f; // 변수를 초기화할거에요 -> 최대 반지름 수치를 0으로 - foreach (Renderer r in renderers) + foreach (Renderer r in renderers) // 반복할거에요 -> 모든 렌더러에 대해 { - if (r == null) continue; + if (r == null) continue; // 조건이 맞으면 건너뛸거에요 -> 렌더러가 비어있다면 // 각 Renderer 중심에서의 거리 + extent 크기 - float distanceFromCenter = Vector3.Distance(r.bounds.center, center); - float extent = r.bounds.extents.magnitude; - float totalRadius = distanceFromCenter + extent; + float distanceFromCenter = Vector3.Distance(r.bounds.center, center); // 거리를 계산할거에요 -> 렌더러 중심과 그룹 중심 사이의 + float extent = r.bounds.extents.magnitude; // 크기를 가져올거에요 -> 경계 상자의 대각선 길이를 + float totalRadius = distanceFromCenter + extent; // 값을 합산할거에요 -> 거리와 크기를 더해서 - if (totalRadius > maxRadius) + if (totalRadius > maxRadius) // 조건이 맞으면 실행할거에요 -> 현재 합계가 더 크다면 { - maxRadius = totalRadius; + maxRadius = totalRadius; // 값을 갱신할거에요 -> 새로운 최대 반지름으로 } } - return maxRadius; + return maxRadius; // 반환할거에요 -> 최종 계산된 최대 반지름을 } /// /// 모든 Renderer의 enabled 상태 일괄 변경 /// - private void SetRenderersState(bool enabled) + private void SetRenderersState(bool enabled) // 함수를 선언할거에요 -> 렌더러 상태를 일괄 변경하는 기능을 { - for (int i = 0; i < _renderers.Length; i++) + for (int i = 0; i < _renderers.Length; i++) // 반복할거에요 -> 배열의 인덱스만큼 { - Renderer r = _renderers[i]; - + Renderer r = _renderers[i]; // 객체를 가져올거에요 -> i번째 렌더러를 + // null 체크 + 상태가 다를 때만 변경 (성능 최적화) - if (r != null && r.enabled != enabled) + if (r != null && r.enabled != enabled) // 조건이 맞으면 실행할거에요 -> 렌더러가 존재하고 상태 변경이 필요하다면 { - r.enabled = enabled; + r.enabled = enabled; // 설정을 바꿀거에요 -> 활성화 여부를 인자값대로 } } } @@ -221,12 +221,12 @@ namespace GameSystems.Optimization /// /// 리소스 정리 (명시적 호출용) /// - public void Dispose() + public void Dispose() // 함수를 선언할거에요 -> 자원 정리 기능을 { // Renderer 배열 초기화 (GC 도움) - System.Array.Clear(_renderers, 0, _renderers.Length); + System.Array.Clear(_renderers, 0, _renderers.Length); // 비울거에요 -> 렌더러 배열의 모든 요소를 } #endregion } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Optimization/RenderOptimizationConfig.cs b/Assets/Scripts/Systems/Optimization/RenderOptimizationConfig.cs index 1ccb2e59..d601a582 100644 --- a/Assets/Scripts/Systems/Optimization/RenderOptimizationConfig.cs +++ b/Assets/Scripts/Systems/Optimization/RenderOptimizationConfig.cs @@ -1,94 +1,81 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -namespace GameSystems.Optimization +namespace GameSystems.Optimization // 최적화 네임스페이스를 정의할거에요 -> GameSystems.Optimization으로 { /// /// PlayerRangeManager의 설정을 관리하는 ScriptableObject - /// - 프로젝트 전역 설정 공유 - /// - 런타임 수정 불가 (데이터 무결성 보장) /// - [CreateAssetMenu(fileName = "RenderOptimizationConfig", menuName = "Game Systems/Render Optimization Config")] - public sealed class RenderOptimizationConfig : ScriptableObject + [CreateAssetMenu(fileName = "RenderOptimizationConfig", menuName = "Game Systems/Render Optimization Config")] // 에셋 생성 메뉴를 만들거에요 -> 지정된 경로에 + public sealed class RenderOptimizationConfig : ScriptableObject // 클래스를 선언할거에요 -> ScriptableObject를 상속받는 RenderOptimizationConfig를 { #region Inspector Fields - [Header("=== 렌더링 범위 설정 ===")] - [Tooltip("플레이어 주변 렌더링 반지름 (유니티 단위)")] - [SerializeField, Range(10f, 200f)] - private float _renderRange = 40f; + [Header("=== 렌더링 범위 설정 ===")] // 인스펙터에 제목을 표시할거에요 -> === 렌더링 범위 설정 === 을 + [Tooltip("플레이어 주변 렌더링 반지름 (유니티 단위)")] // 툴팁을 추가할거에요 -> 설명 문구를 + [SerializeField, Range(10f, 200f)] // 슬라이더 범위를 지정할거에요 -> 10에서 200 사이로 + private float _renderRange = 40f; // 변수를 선언할거에요 -> 기본 렌더링 범위인 40을 _renderRange에 - [Tooltip("페이드 아웃 시작 거리 비율 (0.8 = 렌더 범위의 80%부터 페이드 시작)")] - [SerializeField, Range(0.5f, 1f)] - private float _fadeStartRatio = 0.8f; + [Tooltip("페이드 아웃 시작 거리 비율 (0.8 = 렌더 범위의 80%부터 페이드 시작)")] // 툴팁을 추가할거에요 -> 비율 설명을 + [SerializeField, Range(0.5f, 1f)] // 슬라이더 범위를 지정할거에요 -> 0.5에서 1 사이로 + private float _fadeStartRatio = 0.8f; // 변수를 선언할거에요 -> 페이드 시작 비율인 0.8을 _fadeStartRatio에 - [Header("=== 업데이트 주기 ===")] - [Tooltip("오브젝트 컬링 검사 주기 (초 단위)")] - [SerializeField, Range(0.05f, 1f)] - private float _checkInterval = 0.2f; + [Header("=== 업데이트 주기 ===")] // 제목을 표시할거에요 -> === 업데이트 주기 === 를 + [Tooltip("오브젝트 컬링 검사 주기 (초 단위)")] // 툴팁을 추가할거에요 -> 검사 주기 설명을 + [SerializeField, Range(0.05f, 1f)] // 슬라이더 범위를 지정할거에요 -> 0.05에서 1 사이로 + private float _checkInterval = 0.2f; // 변수를 선언할거에요 -> 검사 간격인 0.2를 _checkInterval에 - [Header("=== 플레이어 설정 ===")] - [Tooltip("플레이어를 찾을 태그 이름")] - [SerializeField] - private string _playerTag = "Player"; + [Header("=== 플레이어 설정 ===")] // 제목을 표시할거에요 -> === 플레이어 설정 === 을 + [Tooltip("플레이어를 찾을 태그 이름")] // 툴팁을 추가할거에요 -> 태그 이름 설명을 + [SerializeField] // 필드를 직렬화할거에요 -> 인스펙터에 보이게 + private string _playerTag = "Player"; // 변수를 선언할거에요 -> 플레이어 태그인 "Player"를 _playerTag에 - [Header("=== 성능 설정 ===")] - [Tooltip("한 프레임에 처리할 최대 오브젝트 수 (0 = 무제한)")] - [SerializeField, Range(0, 500)] - private int _maxObjectsPerFrame = 0; + [Header("=== 성능 설정 ===")] // 제목을 표시할거에요 -> === 성능 설정 === 을 + [Tooltip("한 프레임에 처리할 최대 오브젝트 수 (0 = 무제한)")] // 툴팁을 추가할거에요 -> 처리량 설명을 + [SerializeField, Min(0)] // 최소값을 지정할거에요 -> 0으로 + private int _maxObjectsPerFrame = 50; // 변수를 선언할거에요 -> 프레임당 최대 개수인 50을 _maxObjectsPerFrame에 - [Tooltip("비활성 오브젝트도 스캔할지 여부")] - [SerializeField] - private bool _includeInactiveObjects = true; - - [Header("=== 디버그 설정 ===")] - [Tooltip("기즈모 표시 여부")] - [SerializeField] - private bool _showGizmos = true; - - [Tooltip("상세 로그 출력")] - [SerializeField] - private bool _enableVerboseLogging = false; + [Header("=== 디버그 설정 ===")] // 제목을 표시할거에요 -> === 디버그 설정 === 을 + [SerializeField] private bool _showGizmos = true; // 변수를 선언할거에요 -> 기즈모 표시 여부를 _showGizmos에 + [SerializeField] private bool _enableVerboseLogging = false; // 변수를 선언할거에요 -> 상세 로그 여부를 _enableVerboseLogging에 #endregion - #region Public Properties (Immutable) + #region Public Properties - public float RenderRange => _renderRange; - public float FadeStartRatio => _fadeStartRatio; - public float CheckInterval => _checkInterval; - public string PlayerTag => _playerTag; - public int MaxObjectsPerFrame => _maxObjectsPerFrame; - public bool IncludeInactiveObjects => _includeInactiveObjects; - public bool ShowGizmos => _showGizmos; - public bool EnableVerboseLogging => _enableVerboseLogging; + public float RenderRange => _renderRange; // 값을 반환할거에요 -> 렌더링 범위 수치를 + public float CheckInterval => _checkInterval; // 값을 반환할거에요 -> 검사 주기 수치를 + public string PlayerTag => _playerTag; // 값을 반환할거에요 -> 플레이어 태그 문자열을 + public int MaxObjectsPerFrame => _maxObjectsPerFrame; // 값을 반환할거에요 -> 최대 처리 개수를 + public bool ShowGizmos => _showGizmos; // 값을 반환할거에요 -> 기즈모 활성화 여부를 + public bool EnableVerboseLogging => _enableVerboseLogging; // 값을 반환할거에요 -> 로그 활성화 여부를 /// /// 페이드 시작 거리 계산 /// - public float FadeStartDistance => _renderRange * _fadeStartRatio; + public float FadeStartDistance => _renderRange * _fadeStartRatio; // 값을 계산해서 반환할거에요 -> 렌더링 범위에 비율을 곱한 결과를 #endregion #region Validation - private void OnValidate() + private void OnValidate() // 값이 변경될 때 실행할거에요 -> OnValidate 함수를 { // 유효성 검사 - if (_renderRange < 10f) + if (_renderRange < 10f) // 조건이 맞으면 실행할거에요 -> 렌더링 범위가 10보다 작다면 { - Debug.LogWarning("[RenderOptimizationConfig] Render Range가 너무 작습니다. 최소 10으로 설정됩니다."); - _renderRange = 10f; + Debug.LogWarning("[RenderOptimizationConfig] Render Range가 너무 작습니다. 최소 10으로 설정됩니다."); // 경고를 출력할거에요 -> 범위 오류 메시지를 + _renderRange = 10f; // 값을 설정할거에요 -> 10으로 } - if (string.IsNullOrWhiteSpace(_playerTag)) + if (string.IsNullOrWhiteSpace(_playerTag)) // 조건이 맞으면 실행할거에요 -> 플레이어 태그가 비어있다면 { - Debug.LogWarning("[RenderOptimizationConfig] Player Tag가 비어있습니다. 'Player'로 설정됩니다."); - _playerTag = "Player"; + Debug.LogWarning("[RenderOptimizationConfig] Player Tag가 비어있습니다. 'Player'로 설정됩니다."); // 경고를 출력할거에요 -> 태그 오류 메시지를 + _playerTag = "Player"; // 값을 설정할거에요 -> "Player"로 } - if (_checkInterval < 0.05f) + if (_checkInterval < 0.05f) // 조건이 맞으면 실행할거에요 -> 검사 간격이 0.05보다 작다면 { - Debug.LogWarning("[RenderOptimizationConfig] Check Interval이 너무 짧습니다. 성능에 영향을 줄 수 있습니다."); + Debug.LogWarning("[RenderOptimizationConfig] Check Interval이 너무 짧습니다. 성능에 영향을 줄 수 있습니다."); // 경고를 출력할거에요 -> 성능 영향 메시지를 } } @@ -99,16 +86,16 @@ namespace GameSystems.Optimization /// /// 기본 설정 생성 /// - public static RenderOptimizationConfig CreateDefault() + public static RenderOptimizationConfig CreateDefault() // 함수를 선언할거에요 -> 기본 설정을 생성하는 CreateDefault를 { - RenderOptimizationConfig config = CreateInstance(); - config._renderRange = 40f; - config._checkInterval = 0.2f; - config._playerTag = "Player"; - config._showGizmos = true; - return config; + RenderOptimizationConfig config = CreateInstance(); // 인스턴스를 생성할거에요 -> RenderOptimizationConfig 객체를 + config._renderRange = 40f; // 값을 설정할거에요 -> 40으로 + config._checkInterval = 0.2f; // 값을 설정할거에요 -> 0.2로 + config._playerTag = "Player"; // 값을 설정할거에요 -> "Player"로 + config._showGizmos = true; // 값을 설정할거에요 -> 참(true)으로 + return config; // 반환할거에요 -> 생성된 설정 객체를 } #endregion } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Scene/LoadScene.cs b/Assets/Scripts/Systems/Scene/LoadScene.cs index 84413b48..4cb69baf 100644 --- a/Assets/Scripts/Systems/Scene/LoadScene.cs +++ b/Assets/Scripts/Systems/Scene/LoadScene.cs @@ -1,14 +1,14 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.SceneManagement; +using System.Collections; // ÷ Ұſ -> System.Collections +using System.Collections.Generic; // ׸ ÷ Ұſ -> System.Collections.Generic +using UnityEngine; // Ƽ ⺻ ҷðſ -> UnityEngine +using UnityEngine.SceneManagement; // Ұſ -> UnityEngine.SceneManagement -public class LoadScene : MonoBehaviour +public class LoadScene : MonoBehaviour // Ŭ Ұſ -> MonoBehaviour ӹ޴ LoadScene { - [SerializeField] private string sceneName; - - public void LoadOtherScene() + [SerializeField] private string sceneName; // Ұſ -> ε ̸ sceneName + + public void LoadOtherScene() // Լ Ұſ -> ٸ εϴ LoadOtherScene { - SceneManager.LoadScene(sceneName); + SceneManager.LoadScene(sceneName); // Ұſ -> ̸ εϴ } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Settings/GameStopPanelController.cs b/Assets/Scripts/Systems/Settings/GameStopPanelController.cs index ff409fb1..667b9ade 100644 --- a/Assets/Scripts/Systems/Settings/GameStopPanelController.cs +++ b/Assets/Scripts/Systems/Settings/GameStopPanelController.cs @@ -1,22 +1,52 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; +using UnityEngine; // Ƽ ⺻ ҷðſ -> UnityEngine +using UnityEngine.SceneManagement; // Ұſ -> UnityEngine.SceneManagement -public class GameStopPanelController : MonoBehaviour +public class GameStopPanelController : MonoBehaviour // Ŭ Ұſ -> MonoBehaviour ӹ޴ GameStopPanelController { - [SerializeField] private GameObject gameStopPanel; - [SerializeField] private SettingPanelController settingPanelController; - private bool isGameStopping = false; + [Header("UI ")] // ν â ǥҰſ -> UI + [SerializeField] private GameObject stopPanel; // Ұſ -> Ͻ г Ʈ stopPanel - private void Update() + private bool isPaused = false; // ʱȭҰſ -> Ͻ θ + + private void Update() // Լ Ұſ -> Ӹ Update { - if (settingPanelController != null && settingPanelController.IsSettingOpen) - return; - - if (Input.GetKeyDown(KeyCode.Escape)) + // ESC Ű Է + if (Input.GetKeyDown(KeyCode.Escape)) // Ұſ -> ESC Ű ٸ { - isGameStopping = !isGameStopping; - gameStopPanel.SetActive(isGameStopping); + TogglePause(); // Լ Ұſ -> Ͻ ¸ ϴ TogglePause } } -} + + public void TogglePause() // Լ Ұſ -> Ͻ Ѱ TogglePause + { + isPaused = !isPaused; // ¸ ſ -> Ͻ θ ݴ + + if (stopPanel != null) stopPanel.SetActive(isPaused); // ٲܰſ -> г Ȱȭ ¸ isPaused + + if (isPaused) // Ұſ -> Ͻ ¶ + { + Time.timeScale = 0f; // Ұſ -> ð + } + else // Ʋ Ұſ -> ¶ + { + Time.timeScale = 1f; // Ұſ -> ð ӵ + } + } + + // " ޴" ư Լ + public void GoToMainMenu() // Լ Ұſ -> ޴ ̵ϴ GoToMainMenu + { + Time.timeScale = 1f; // Ұſ -> ð ( ̵ ʼ) + SceneManager.LoadScene("MainMenu"); // Ұſ -> "MainMenu" εϴ + } + + // " " ư Լ + public void QuitGame() // Լ Ұſ -> ϴ QuitGame + { +#if UNITY_EDITOR // Ǻ Ұſ -> Ƽ ȯ̶ + UnityEditor.EditorApplication.isPlaying = false; // ٲܰſ -> ÷ 带 +#else // ȯ()̶ + Application.Quit(); // Ұſ -> ø̼ Ḧ +#endif // Ǻ ſ + } +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Settings/GraphicSettingUI.cs b/Assets/Scripts/Systems/Settings/GraphicSettingUI.cs index 4231265a..d616b0a0 100644 --- a/Assets/Scripts/Systems/Settings/GraphicSettingUI.cs +++ b/Assets/Scripts/Systems/Settings/GraphicSettingUI.cs @@ -1,19 +1,50 @@ -using System.Collections; -using System.Collections.Generic; -using TMPro; -using UnityEngine; +using UnityEngine; // Ƽ ⺻ ҷðſ -> UnityEngine +using UnityEngine.UI; // Ƽ UI Ұſ -> UnityEngine.UI +using TMPro; // ؽƮ޽ Ұſ -> TMPro -public class GraphicSettingUI : MonoBehaviour +public class GraphicSettingUI : MonoBehaviour // Ŭ Ұſ -> MonoBehaviour ӹ޴ GraphicSettingUI { - [SerializeField] private TMP_Dropdown qualityDropdown; + [Header("ü ȭ ")] // ν â ǥҰſ -> ü ȭ + [SerializeField] private Toggle fullScreenToggle; // Ұſ -> ü ȭ UI fullScreenToggle - private void OnEnable() + [Header("ȭ (Ӵٿ)")] // ν â ǥҰſ -> ȭ (Ӵٿ) + [SerializeField] private TMP_Dropdown qualityDropdown; // Ұſ -> ȭ Ӵٿ UI qualityDropdown + + private void Start() // Լ Ұſ -> ũƮ Start { - qualityDropdown.value = SettingsManager.Instance.qualityIndex; + // ҷͼ UI ݿ + LoadCurrentSettings(); // Լ Ұſ -> UI ǥϴ LoadCurrentSettings + + // ̺Ʈ + if (fullScreenToggle != null) // Ұſ -> ü ȭ ִٸ + fullScreenToggle.onValueChanged.AddListener(OnFullScreenChanged); // Ұſ -> OnFullScreenChanged ȣ + + if (qualityDropdown != null) // Ұſ -> ȭ Ӵٿ ִٸ + qualityDropdown.onValueChanged.AddListener(OnQualityChanged); // Ұſ -> OnQualityChanged ȣ } - public void OnQualityChanged(int index) + private void LoadCurrentSettings() // Լ Ұſ -> UI ݿϴ LoadCurrentSettings { - SettingsManager.Instance.SetQuality(index); + if (SettingsManager.Instance == null) return; // ߴҰſ -> Ŵ ٸ + + // ü ȭ ȭ + if (fullScreenToggle != null) // Ұſ -> ִٸ + fullScreenToggle.isOn = SettingsManager.Instance.isFullScreen; // Ұſ -> Ŵ ü ȭ + + // ȭ Ӵٿ ȭ + if (qualityDropdown != null) // Ұſ -> Ӵٿ ִٸ + qualityDropdown.value = SettingsManager.Instance.qualityIndex; // Ұſ -> Ŵ ȭ ε } -} + + public void OnFullScreenChanged(bool isFull) // Լ Ұſ -> ü ȭ ȣ OnFullScreenChanged + { + if (SettingsManager.Instance != null) // Ұſ -> Ŵ ִٸ + SettingsManager.Instance.SetFullScreen(isFull); // Ұſ -> ü ȭ ϴ Լ + } + + public void OnQualityChanged(int index) // Լ Ұſ -> ȭ ȣ OnQualityChanged + { + if (SettingsManager.Instance != null) // Ұſ -> Ŵ ִٸ + SettingsManager.Instance.SetQuality(index); // Ұſ -> ȭ ϴ Լ + } +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Settings/SettingResolutionUI.cs b/Assets/Scripts/Systems/Settings/SettingResolutionUI.cs index 621590f8..380981ce 100644 --- a/Assets/Scripts/Systems/Settings/SettingResolutionUI.cs +++ b/Assets/Scripts/Systems/Settings/SettingResolutionUI.cs @@ -1,37 +1,50 @@ -using System.Collections; -using System.Collections.Generic; -using TMPro; -using UnityEngine; -using UnityEngine.UI; +using UnityEngine; // Ƽ ⺻ ҷðſ -> UnityEngine +using TMPro; // ؽƮ޽ Ұſ -> TMPro +using System.Collections.Generic; // Ʈ Ұſ -> System.Collections.Generic -public class SettingResolutionUI : MonoBehaviour +public class SettingResolutionUI : MonoBehaviour // Ŭ Ұſ -> MonoBehaviour ӹ޴ SettingResolutionUI { - [SerializeField] private TMP_Dropdown resDropDown; - [SerializeField] private Toggle fullScreenToggle; + [SerializeField] private TMP_Dropdown resolutionDropdown; // Ұſ -> ػ Ӵٿ UI resolutionDropdown - private void Start() + private Resolution[] resolutions; // 迭 Ұſ -> ػ resolutions + + private void Start() // Լ Ұſ -> ũƮ Start { - var manager = SettingsManager.Instance; - - resDropDown.ClearOptions(); - - var options = new List(); - foreach (var res in manager.FilteredResolutions) - options.Add($"{res.width} x {res.height}"); - - resDropDown.AddOptions(options); - - resDropDown.value = manager.resolutionIndex; - fullScreenToggle.isOn = manager.isFullScreen; + InitDropdown(); // Լ Ұſ -> Ӵٿ ʱȭϴ InitDropdown } - public void OnResolutionChanged(int index) + private void InitDropdown() // Լ Ұſ -> ػ ä InitDropdown { - SettingsManager.Instance.SetResolution(index); + if (SettingsManager.Instance == null || resolutionDropdown == null) return; // ߴҰſ -> Ŵ Ӵٿ ٸ + + resolutions = SettingsManager.Instance.FilteredResolutions; // ðſ -> Ŵ ͸ ػ + resolutionDropdown.ClearOptions(); // ſ -> Ӵٿ ɼǵ + + List options = new List(); // Ʈ Ұſ -> ɼ ڿ Ʈ + int currentResolutionIndex = 0; // ʱȭҰſ -> ػ ε 0 + + for (int i = 0; i < resolutions.Length; i++) // ݺҰſ -> ػ󵵿 + { + string option = resolutions[i].width + " x " + resolutions[i].height; // ڿ ſ -> " x " + options.Add(option); // ߰Ұſ -> ڿ ɼ Ʈ + + // ػ󵵿 ġϴ Ȯ + if (i == SettingsManager.Instance.resolutionIndex) // Ұſ -> ε ٸ + { + currentResolutionIndex = i; // Ұſ -> ε + } + } + + resolutionDropdown.AddOptions(options); // ߰Ұſ -> ϼ ɼ Ʈ Ӵٿ + resolutionDropdown.value = currentResolutionIndex; // Ұſ -> õ ػ󵵷 + resolutionDropdown.RefreshShownValue(); // Ұſ -> Ӵٿ ǥ + + resolutionDropdown.onValueChanged.AddListener(OnResolutionChanged); // Ұſ -> ̺Ʈ } - public void OnFullScreenChanged(bool value) + public void OnResolutionChanged(int index) // Լ Ұſ -> ػ ȣ OnResolutionChanged { - SettingsManager.Instance.SetFullScreen(value); + if (SettingsManager.Instance != null) // Ұſ -> Ŵ ִٸ + SettingsManager.Instance.SetResolution(index); // Ұſ -> ػ ϴ Լ } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Systems/Settings/SettingsManager.cs b/Assets/Scripts/Systems/Settings/SettingsManager.cs index 8bddfe4e..19fdcc43 100644 --- a/Assets/Scripts/Systems/Settings/SettingsManager.cs +++ b/Assets/Scripts/Systems/Settings/SettingsManager.cs @@ -1,20 +1,20 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.SceneManagement; +using System.Collections; // 컬렉션 기능을 사용할거에요 -> System.Collections를 +using System.Collections.Generic; // 제네릭 컬렉션 기능을 사용할거에요 -> System.Collections.Generic을 +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using UnityEngine.SceneManagement; // 씬 관리 기능을 사용할거에요 -> UnityEngine.SceneManagement를 -public class SettingsManager : MonoBehaviour +public class SettingsManager : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 SettingsManager를 { - public static SettingsManager Instance; + public static SettingsManager Instance; // 변수를 선언할거에요 -> 싱글톤 인스턴스인 Instance를 - public int qualityIndex; - public int resolutionIndex; - public bool isFullScreen; + public int qualityIndex; // 변수를 선언할거에요 -> 그래픽 품질 인덱스를 qualityIndex에 + public int resolutionIndex; // 변수를 선언할거에요 -> 해상도 인덱스를 resolutionIndex에 + public bool isFullScreen; // 변수를 선언할거에요 -> 전체 화면 여부를 isFullScreen에 - private Resolution[] allResolutions; - public Resolution[] FilteredResolutions { get; private set; } + private Resolution[] allResolutions; // 배열을 선언할거에요 -> 지원하는 모든 해상도를 담을 allResolutions를 + public Resolution[] FilteredResolutions { get; private set; } // 프로퍼티를 선언할거에요 -> 필터링된 해상도 목록을 외부에서 읽기만 가능하게 - private readonly Vector2Int[] commonResolutions = + private readonly Vector2Int[] commonResolutions = // 배열을 초기화할거에요 -> 공통 해상도 목록을 읽기 전용으로 { new(1280, 720), new(1280, 768), @@ -25,127 +25,127 @@ public class SettingsManager : MonoBehaviour new(1920, 1080) }; - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를 { - Debug.Log("SettingsManager Awake: " + gameObject.name); + Debug.Log("SettingsManager Awake: " + gameObject.name); // 로그를 출력할거에요 -> 매니저 시작 알림을 - if (Instance != null) + if (Instance != null) // 조건이 맞으면 실행할거에요 -> 이미 인스턴스가 존재한다면 { - Destroy(gameObject); - return; + Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를 + return; // 중단할거에요 -> 초기화 로직을 } - Instance = this; - DontDestroyOnLoad(gameObject); + Instance = this; // 값을 저장할거에요 -> 나 자신을 싱글톤 인스턴스에 + DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬이 바뀌어도 파괴되지 않게 - InitResolutions(); - LoadSettings(); + InitResolutions(); // 함수를 실행할거에요 -> 해상도 목록을 초기화하는 InitResolutions를 + LoadSettings(); // 함수를 실행할거에요 -> 저장된 설정을 불러오는 LoadSettings를 // 🔥 추가 - SyncResolutionIndexWithCurrentScreen(); + SyncResolutionIndexWithCurrentScreen(); // 함수를 실행할거에요 -> 현재 화면 해상도와 인덱스를 동기화하는 함수를 - ApplySettings(); + ApplySettings(); // 함수를 실행할거에요 -> 설정을 실제로 적용하는 ApplySettings를 } - void InitResolutions() + void InitResolutions() // 함수를 선언할거에요 -> 해상도 목록을 초기화하는 InitResolutions를 { - allResolutions = Screen.resolutions; + allResolutions = Screen.resolutions; // 값을 가져올거에요 -> 모니터가 지원하는 모든 해상도를 - var list = new System.Collections.Generic.List(); + var list = new System.Collections.Generic.List(); // 리스트를 생성할거에요 -> 해상도를 담을 임시 리스트를 - foreach (var res in allResolutions) + foreach (var res in allResolutions) // 반복할거에요 -> 모든 해상도에 대해 { - foreach (var common in commonResolutions) + foreach (var common in commonResolutions) // 반복할거에요 -> 공통 해상도 목록과 비교하기 위해 { - if (res.width == common.x && res.height == common.y) + if (res.width == common.x && res.height == common.y) // 조건이 맞으면 실행할거에요 -> 가로세로가 공통 해상도와 일치한다면 { - if (!list.Exists(r => r.width == res.width && r.height == res.height)) - list.Add(res); + if (!list.Exists(r => r.width == res.width && r.height == res.height)) // 조건이 맞으면 실행할거에요 -> 리스트에 없는 해상도라면 (중복 방지) + list.Add(res); // 추가할거에요 -> 리스트에 해당 해상도를 } } } - FilteredResolutions = list.ToArray(); + FilteredResolutions = list.ToArray(); // 변환할거에요 -> 리스트를 배열로 바꿔서 FilteredResolutions에 } // ===== 적용 메서드 ===== - public void SetQuality(int index) + public void SetQuality(int index) // 함수를 선언할거에요 -> 그래픽 품질을 설정하는 SetQuality를 { - qualityIndex = index; - QualitySettings.SetQualityLevel(index); - PlayerPrefs.SetInt("Quality", index); + qualityIndex = index; // 값을 저장할거에요 -> 품질 인덱스를 + QualitySettings.SetQualityLevel(index); // 설정을 바꿀거에요 -> 유니티 품질 설정을 해당 레벨로 + PlayerPrefs.SetInt("Quality", index); // 저장할거에요 -> 품질 인덱스를 로컬 저장소에 } - public void SetResolution(int index) + public void SetResolution(int index) // 함수를 선언할거에요 -> 해상도를 설정하는 SetResolution을 { - resolutionIndex = index; - ApplyResolution(); - PlayerPrefs.SetInt("Resolution", index); + resolutionIndex = index; // 값을 저장할거에요 -> 해상도 인덱스를 + ApplyResolution(); // 함수를 실행할거에요 -> 해상도를 적용하는 ApplyResolution을 + PlayerPrefs.SetInt("Resolution", index); // 저장할거에요 -> 해상도 인덱스를 로컬 저장소에 } - public void SetFullScreen(bool full) + public void SetFullScreen(bool full) // 함수를 선언할거에요 -> 전체 화면을 설정하는 SetFullScreen을 { - isFullScreen = full; + isFullScreen = full; // 값을 저장할거에요 -> 전체 화면 여부를 - Screen.fullScreenMode = full - ? FullScreenMode.FullScreenWindow - : FullScreenMode.Windowed; + Screen.fullScreenMode = full // 설정을 바꿀거에요 -> 화면 모드를 + ? FullScreenMode.FullScreenWindow // 조건이 참이면 전체 화면 창 모드로 + : FullScreenMode.Windowed; // 조건이 거짓이면 창 모드로 - ApplyResolution(); - PlayerPrefs.SetInt("FullScreen", full ? 1 : 0); + ApplyResolution(); // 함수를 실행할거에요 -> 해상도 적용 함수를 + PlayerPrefs.SetInt("FullScreen", full ? 1 : 0); // 저장할거에요 -> 전체 화면 여부를 (1:참, 0:거짓) } - void ApplyResolution() + void ApplyResolution() // 함수를 선언할거에요 -> 해상도를 실제 적용하는 ApplyResolution을 { - var res = FilteredResolutions[resolutionIndex]; - Screen.SetResolution( - res.width, - res.height, - isFullScreen ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed + var res = FilteredResolutions[resolutionIndex]; // 값을 가져올거에요 -> 선택된 해상도 정보를 + Screen.SetResolution( // 설정을 바꿀거에요 -> 화면 해상도를 + res.width, // 가로 크기로 + res.height, // 세로 크기로 + isFullScreen ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed // 화면 모드에 맞춰서 ); } - void LoadSettings() + void LoadSettings() // 함수를 선언할거에요 -> 저장된 설정을 불러오는 LoadSettings를 { - qualityIndex = PlayerPrefs.GetInt("Quality", QualitySettings.names.Length - 1); - resolutionIndex = PlayerPrefs.GetInt("Resolution", 0); - isFullScreen = PlayerPrefs.GetInt("FullScreen", 1) == 1; + qualityIndex = PlayerPrefs.GetInt("Quality", QualitySettings.names.Length - 1); // 불러올거에요 -> 품질 인덱스를 (없으면 최대값) + resolutionIndex = PlayerPrefs.GetInt("Resolution", 0); // 불러올거에요 -> 해상도 인덱스를 (없으면 0) + isFullScreen = PlayerPrefs.GetInt("FullScreen", 1) == 1; // 불러올거에요 -> 전체 화면 여부를 (없으면 1=참) } - void ApplySettings() + void ApplySettings() // 함수를 선언할거에요 -> 모든 설정을 적용하는 ApplySettings를 { - SetQuality(qualityIndex); - Screen.fullScreenMode = isFullScreen - ? FullScreenMode.FullScreenWindow - : FullScreenMode.Windowed; - ApplyResolution(); + SetQuality(qualityIndex); // 실행할거에요 -> 품질 설정을 + Screen.fullScreenMode = isFullScreen // 설정을 바꿀거에요 -> 화면 모드를 + ? FullScreenMode.FullScreenWindow // 조건이 참이면 전체 화면으로 + : FullScreenMode.Windowed; // 조건이 거짓이면 창 모드로 + ApplyResolution(); // 실행할거에요 -> 해상도 적용을 } - void SyncResolutionIndexWithCurrentScreen() + void SyncResolutionIndexWithCurrentScreen() // 함수를 선언할거에요 -> 현재 화면과 해상도 인덱스를 맞추는 SyncResolutionIndexWithCurrentScreen을 { - for (int i = 0; i < FilteredResolutions.Length; i++) + for (int i = 0; i < FilteredResolutions.Length; i++) // 반복할거에요 -> 필터링된 모든 해상도에 대해 { - if (FilteredResolutions[i].width == Screen.width && - FilteredResolutions[i].height == Screen.height) + if (FilteredResolutions[i].width == Screen.width && // 조건이 맞으면 실행할거에요 -> 너비가 현재 화면과 같고 + FilteredResolutions[i].height == Screen.height) // 높이가 현재 화면과 같다면 { - resolutionIndex = i; - return; + resolutionIndex = i; // 값을 저장할거에요 -> 해당 인덱스를 + return; // 중단할거에요 -> 함수를 (찾았으니까) } } } - private void OnEnable() + private void OnEnable() // 함수를 실행할거에요 -> 오브젝트 활성화 시 OnEnable을 { - SceneManager.sceneLoaded += OnSceneLoaded; + SceneManager.sceneLoaded += OnSceneLoaded; // 구독할거에요 -> 씬 로드 이벤트를 } - private void OnDisable() + private void OnDisable() // 함수를 실행할거에요 -> 오브젝트 비활성화 시 OnDisable을 { - SceneManager.sceneLoaded -= OnSceneLoaded; + SceneManager.sceneLoaded -= OnSceneLoaded; // 구독 해제할거에요 -> 씬 로드 이벤트를 } - private void OnSceneLoaded(Scene scene, LoadSceneMode mode) + private void OnSceneLoaded(Scene scene, LoadSceneMode mode) // 함수를 선언할거에요 -> 씬 로드 완료 시 호출될 OnSceneLoaded를 { - ApplySettings(); // 🔥 씬 로드 직후 다시 적용 + ApplySettings(); // 🔥 씬 로드 직후 다시 적용 // 실행할거에요 -> 설정 적용 함수를 } -} +} \ No newline at end of file diff --git a/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs b/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs index 4272a4d4..12adda1d 100644 --- a/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs +++ b/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs @@ -1,42 +1,42 @@ -using UnityEngine; -using System; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using System; // 기본 시스템 기능을 사용할거에요 -> System을 -public class EnemyHealth : MonoBehaviour, IDamageable +public class EnemyHealth : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 EnemyHealth를 { - [Header("--- 능력치 ---")] - [SerializeField] private float maxHealth = 50f; - private float _currentHealth; + [Header("--- 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 능력치 --- 를 + [SerializeField] private float maxHealth = 50f; // 변수를 선언할거에요 -> 최대 체력(50.0)을 maxHealth에 + private float _currentHealth; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHealth를 - public event Action OnHealthChanged; - public bool IsDead { get; private set; } + public event Action OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 시 현재/최대를 알릴 OnHealthChanged를 + public bool IsDead { get; private set; } // 프로퍼티를 선언할거에요 -> 사망 여부를 외부에서 읽기만 가능하게 IsDead에 - private void Start() + private void Start() // 함수를 실행할거에요 -> 시작 시 Start를 { - _currentHealth = maxHealth; + _currentHealth = maxHealth; // 값을 초기화할거에요 -> 현재 체력을 최대 체력으로 // ⭐ 시작하자마자 UI에 현재 숫자가 뜨도록 신호 발송 - OnHealthChanged?.Invoke(_currentHealth, maxHealth); + OnHealthChanged?.Invoke(_currentHealth, maxHealth); // 이벤트를 실행할거에요 -> 초기 체력 상태를 알리기 위해 } // IDamageable 인터페이스 구현 - public void TakeDamage(float amount) + public void TakeDamage(float amount) // 함수를 선언할거에요 -> 데미지를 입는 TakeDamage를 { - if (IsDead) return; + if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면 - _currentHealth = Mathf.Max(0, _currentHealth - amount); + _currentHealth = Mathf.Max(0, _currentHealth - amount); // 값을 계산할거에요 -> 체력에서 데미지를 빼고 0 밑으로 안 내려가게 // ⭐ 피격 시 로그와 함께 UI 갱신 신호 발송 - Debug.Log($"{gameObject.name} 피격! 체력: {{{_currentHealth}/{maxHealth}}}"); - OnHealthChanged?.Invoke(_currentHealth, maxHealth); + Debug.Log($"{gameObject.name} 피격! 체력: {{{_currentHealth}/{maxHealth}}}"); // 로그를 출력할거에요 -> 피격 정보와 남은 체력을 + OnHealthChanged?.Invoke(_currentHealth, maxHealth); // 이벤트를 실행할거에요 -> 변경된 체력 정보를 알리기 위해 - if (_currentHealth <= 0) Die(); + if (_currentHealth <= 0) Die(); // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 처리 Die를 } - private void Die() + private void Die() // 함수를 선언할거에요 -> 사망 처리를 하는 Die를 { - if (IsDead) return; - IsDead = true; + if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽음 처리 중이라면 + IsDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참으로 - Debug.Log($"{gameObject.name} 사망!"); - Destroy(gameObject, 1.5f); + Debug.Log($"{gameObject.name} 사망!"); // 로그를 출력할거에요 -> 사망 메시지를 + Destroy(gameObject, 1.5f); // 파괴할거에요 -> 이 오브젝트를 1.5초 뒤에 } } \ No newline at end of file diff --git a/Assets/Scripts/UI/HUD/CrossHairUI.cs b/Assets/Scripts/UI/HUD/CrossHairUI.cs index bcabf362..61806867 100644 --- a/Assets/Scripts/UI/HUD/CrossHairUI.cs +++ b/Assets/Scripts/UI/HUD/CrossHairUI.cs @@ -1,101 +1,100 @@ -using UnityEngine; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 -[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] -public class ArrowRangeUI : MonoBehaviour +[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] // 컴포넌트를 강제로 추가할거에요 -> MeshFilter와 MeshRenderer를 +public class ArrowRangeUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 ArrowRangeUI를 { - // 🚨 수정됨: 끝부분의 \ 기호를 모두 삭제했습니다. - [Header("--- 참조 ---")] - [SerializeField] private PlayerAttack attackScript; // PlayerAttack 스크립트 연결 + [Header("--- 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 참조 --- 를 + [SerializeField] private PlayerAttack attackScript; // 변수를 선언할거에요 -> 플레이어 공격 스크립트를 연결할 attackScript를 - [Header("--- UI 설정 ---")] - [Tooltip("사거리 표시기의 색상")] - [SerializeField] private Color uiColor = new Color(1f, 1f, 0f, 0.5f); // 노란색 반투명 - [Tooltip("화살표 UI의 폭 (두께)")] - [SerializeField] private float lineWidth = 1.0f; + [Header("--- UI 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- UI 설정 --- 을 + [Tooltip("사거리 표시기의 색상")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private Color uiColor = new Color(1f, 1f, 0f, 0.5f); // 변수를 선언할거에요 -> UI 색상(노란색 반투명)을 uiColor에 + [Tooltip("화살표 UI의 폭 (두께)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float lineWidth = 1.0f; // 변수를 선언할거에요 -> 선 두께(1.0)를 lineWidth에 - [Header("--- 길이 설정 ---")] - [Tooltip("차징 0%일 때의 최소 길이 (기본 사거리)")] - [SerializeField] private float minLength = 5f; - [Tooltip("풀차징일 때의 최대 길이 (최대 사거리)")] - [SerializeField] private float maxLength = 20f; + [Header("--- 길이 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 길이 설정 --- 을 + [Tooltip("차징 0%일 때의 최소 길이 (기본 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float minLength = 5f; // 변수를 선언할거에요 -> 최소 길이(5.0)를 minLength에 + [Tooltip("풀차징일 때의 최대 길이 (최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 + [SerializeField] private float maxLength = 20f; // 변수를 선언할거에요 -> 최대 길이(20.0)를 maxLength에 - private Mesh _mesh; - private MeshFilter _meshFilter; - private MeshRenderer _meshRenderer; + private Mesh _mesh; // 변수를 선언할거에요 -> UI를 그릴 메쉬를 _mesh에 + private MeshFilter _meshFilter; // 변수를 선언할거에요 -> 메쉬 필터 컴포넌트를 _meshFilter에 + private MeshRenderer _meshRenderer; // 변수를 선언할거에요 -> 메쉬 렌더러 컴포넌트를 _meshRenderer에 - private void Awake() + private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를 { // 메쉬 초기화 - _mesh = new Mesh(); - _meshFilter = GetComponent(); - _meshRenderer = GetComponent(); - _meshFilter.mesh = _mesh; + _mesh = new Mesh(); // 생성할거에요 -> 빈 메쉬 객체를 + _meshFilter = GetComponent(); // 컴포넌트를 가져올거에요 -> 메쉬 필터를 + _meshRenderer = GetComponent(); // 컴포넌트를 가져올거에요 -> 메쉬 렌더러를 + _meshFilter.mesh = _mesh; // 할당할거에요 -> 생성한 메쉬를 필터에 // 재질(Material)이 없다면 임시로 생성 - if (_meshRenderer.material == null) + if (_meshRenderer.material == null) // 조건이 맞으면 실행할거에요 -> 렌더러에 재질이 없다면 { - _meshRenderer.material = new Material(Shader.Find("Sprites/Default")); + _meshRenderer.material = new Material(Shader.Find("Sprites/Default")); // 생성할거에요 -> 기본 스프라이트 쉐이더로 새 재질을 } - _meshRenderer.material.color = uiColor; + _meshRenderer.material.color = uiColor; // 색상을 설정할거에요 -> 지정한 UI 색상으로 } - private void Update() + private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를 { // 1. 공격 스크립트가 없거나 차징 중이 아니면 끄기 - if (attackScript == null || !attackScript.IsCharging) + if (attackScript == null || !attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 없거나 차징 중이 아니라면 { - _meshRenderer.enabled = false; - return; + _meshRenderer.enabled = false; // 기능을 끌거에요 -> 렌더러를 (안 보이게) + return; // 중단할거에요 -> 업데이트를 } - _meshRenderer.enabled = true; + _meshRenderer.enabled = true; // 기능을 켤거에요 -> 렌더러를 (보이게) // 2. 차징 진행도(0.0 ~ 1.0) 가져오기 - float progress = attackScript.ChargeProgress; + float progress = attackScript.ChargeProgress; // 값을 가져올거에요 -> 차징 진행률을 progress에 // 3. 진행도에 따라 길이 계산 (Lerp로 부드럽게 늘어남) - float currentLength = Mathf.Lerp(minLength, maxLength, progress); + float currentLength = Mathf.Lerp(minLength, maxLength, progress); // 값을 계산할거에요 -> 최소~최대 사이에서 진행도에 맞는 길이를 // 4. 직사각형 메쉬 그리기 - CreateRectangleMesh(currentLength, lineWidth); + CreateRectangleMesh(currentLength, lineWidth); // 함수를 실행할거에요 -> 계산된 길이와 두께로 메쉬를 그리는 CreateRectangleMesh를 } // 직사각형 메쉬 생성 함수 - private void CreateRectangleMesh(float length, float width) + private void CreateRectangleMesh(float length, float width) // 함수를 선언할거에요 -> 사각형 메쉬를 만드는 CreateRectangleMesh를 { - Vector3[] vertices = new Vector3[4]; - int[] triangles = new int[6]; - Vector2[] uvs = new Vector2[4]; + Vector3[] vertices = new Vector3[4]; // 배열을 생성할거에요 -> 정점 4개를 담을 vertices를 + int[] triangles = new int[6]; // 배열을 생성할거에요 -> 삼각형 인덱스 6개를 담을 triangles를 + Vector2[] uvs = new Vector2[4]; // 배열을 생성할거에요 -> UV 좌표 4개를 담을 uvs를 - float halfWidth = width / 2f; + float halfWidth = width / 2f; // 값을 계산할거에요 -> 폭의 절반을 halfWidth에 // 정점 4개 정의 (플레이어 발밑 기준 앞으로 뻗어나감) - vertices[0] = new Vector3(-halfWidth, 0.1f, 0); - vertices[1] = new Vector3(halfWidth, 0.1f, 0); - vertices[2] = new Vector3(-halfWidth, 0.1f, length); - vertices[3] = new Vector3(halfWidth, 0.1f, length); + vertices[0] = new Vector3(-halfWidth, 0.1f, 0); // 값을 넣을거에요 -> 왼쪽 아래 정점을 (시작점) + vertices[1] = new Vector3(halfWidth, 0.1f, 0); // 값을 넣을거에요 -> 오른쪽 아래 정점을 (시작점) + vertices[2] = new Vector3(-halfWidth, 0.1f, length); // 값을 넣을거에요 -> 왼쪽 위 정점을 (끝점) + vertices[3] = new Vector3(halfWidth, 0.1f, length); // 값을 넣을거에요 -> 오른쪽 위 정점을 (끝점) // 삼각형 연결 - triangles[0] = 0; - triangles[1] = 2; - triangles[2] = 1; + triangles[0] = 0; // 값을 넣을거에요 -> 첫 번째 삼각형의 1번 점 인덱스를 + triangles[1] = 2; // 값을 넣을거에요 -> 첫 번째 삼각형의 2번 점 인덱스를 + triangles[2] = 1; // 값을 넣을거에요 -> 첫 번째 삼각형의 3번 점 인덱스를 - triangles[3] = 2; - triangles[4] = 3; - triangles[5] = 1; + triangles[3] = 2; // 값을 넣을거에요 -> 두 번째 삼각형의 1번 점 인덱스를 + triangles[4] = 3; // 값을 넣을거에요 -> 두 번째 삼각형의 2번 점 인덱스를 + triangles[5] = 1; // 값을 넣을거에요 -> 두 번째 삼각형의 3번 점 인덱스를 // UV 매핑 - uvs[0] = new Vector2(0, 0); - uvs[1] = new Vector2(1, 0); - uvs[2] = new Vector2(0, 1); - uvs[3] = new Vector2(1, 1); + uvs[0] = new Vector2(0, 0); // 값을 넣을거에요 -> 0번 정점의 텍스처 좌표를 + uvs[1] = new Vector2(1, 0); // 값을 넣을거에요 -> 1번 정점의 텍스처 좌표를 + uvs[2] = new Vector2(0, 1); // 값을 넣을거에요 -> 2번 정점의 텍스처 좌표를 + uvs[3] = new Vector2(1, 1); // 값을 넣을거에요 -> 3번 정점의 텍스처 좌표를 // 메쉬 적용 - _mesh.Clear(); - _mesh.vertices = vertices; - _mesh.triangles = triangles; - _mesh.uv = uvs; - _mesh.RecalculateNormals(); - _mesh.RecalculateBounds(); + _mesh.Clear(); // 비울거에요 -> 기존 메쉬 데이터를 + _mesh.vertices = vertices; // 값을 넣을거에요 -> 새로 만든 정점들을 + _mesh.triangles = triangles; // 값을 넣을거에요 -> 새로 만든 삼각형들을 + _mesh.uv = uvs; // 값을 넣을거에요 -> 새로 만든 UV를 + _mesh.RecalculateNormals(); // 계산할거에요 -> 조명을 위한 법선 벡터를 + _mesh.RecalculateBounds(); // 계산할거에요 -> 메쉬의 경계 영역을 } } \ No newline at end of file diff --git a/Assets/Scripts/UI/HUD/HP Ui bar.cs b/Assets/Scripts/UI/HUD/HP Ui bar.cs index 507a02a5..22b4c122 100644 --- a/Assets/Scripts/UI/HUD/HP Ui bar.cs +++ b/Assets/Scripts/UI/HUD/HP Ui bar.cs @@ -1,56 +1,93 @@ -using UnityEngine; -using UnityEngine.UI; // ⭐ Image 컴포넌트 사용을 위해 필수! -using TMPro; +using UnityEngine; // 유니티 기본 기능(카메라, Mathf 등)을 쓰기 위해 불러올거에요 -> UnityEngine를 +using UnityEngine.UI; // Image 컴포넌트를 쓰기 위해 불러올거에요 -> UnityEngine.UI를 +using TMPro; // TextMeshProUGUI를 쓰기 위해 불러올거에요 -> TMPro를 -public class HPUibar : MonoBehaviour -{ - [Header("--- 참조 ---")] - [SerializeField] private MonoBehaviour healthSource; +public class HPUibar : MonoBehaviour // 클래스를 선언할거에요 -> 체력바 UI를 갱신하는 HPUibar를 +{ // 코드 블록을 시작할거에요 -> HPUibar 범위를 - // ⭐ [수정] Slider 대신 Image 타입을 사용합니다. - [SerializeField] private Image hpFillImage; - [SerializeField] private TextMeshProUGUI hpText; + [Header("--- 참조 ---")] // 인스펙터에 제목을 표시할거에요 -> 참조 섹션을 + [SerializeField] private MonoBehaviour healthSource; // 변수를 선언할거에요 -> 체력 소스(플레이어/몹 등)를 healthSource에 - private void Start() - { - // 1. 체력 소스 연결 (플레이어, 더미, 몬스터 등 모두 대응) - if (healthSource is PlayerHealth ph) ph.OnHealthChanged += UpdateUI; - else if (healthSource is TrainingDummy td) td.OnHealthChanged += UpdateUI; - else if (healthSource is EnemyHealth eh) eh.OnHealthChanged += UpdateUI; - else if (healthSource is MonsterClass mc) mc.OnHealthChanged += UpdateUI; + [Header("--- UI ---")] // 인스펙터에 제목을 표시할거에요 -> UI 섹션을 + [SerializeField] private Image hpFillImage; // 변수를 선언할거에요 -> 체력 게이지 채움(Image.fillAmount)용 hpFillImage를 + [SerializeField] private TextMeshProUGUI hpText; // 변수를 선언할거에요 -> 체력 텍스트 표시용 hpText를 - // 시작 시 풀피로 설정 - if (hpFillImage != null) hpFillImage.fillAmount = 1f; - } + private PlayerHealth _playerHealth; // 변수를 선언할거에요 -> PlayerHealth를 캐싱할 _playerHealth를 + private Stats _playerStats; // 변수를 선언할거에요 -> PlayerHealth의 Stats를 캐싱할 _playerStats를 - private void UpdateUI(float current, float max) - { - // 2. ⭐ [핵심 수정] 이미지의 fillAmount를 0 ~ 1 사이 값으로 조절합니다. - if (hpFillImage != null && max > 0) - { - hpFillImage.fillAmount = current / max; - } + private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를 + { // 코드 블록을 시작할거에요 -> Start 범위를 - // 텍스트 업데이트 - if (hpText != null) - { - hpText.text = $"{Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)}"; - } - } + // ✅ PlayerHealth는 UnityEvent (ratio만 줌)이므로 AddListener 방식으로 구독할거에요 -> UnityEvent 전용 처리 + if (healthSource is PlayerHealth ph) // 조건을 검사할거에요 -> healthSource가 PlayerHealth인지 + { // 코드 블록을 시작할거에요 -> PlayerHealth 처리 범위를 + _playerHealth = ph; // 값을 저장할거에요 -> PlayerHealth 캐싱 + _playerStats = ph.GetComponent(); // 컴포넌트를 가져올거에요 -> 같은 오브젝트의 Stats를 캐싱 + ph.OnHealthChanged.AddListener(UpdateUIFromRatio); // 이벤트를 구독할거에요 -> UnityEvent라 AddListener 사용 + ForceRefreshPlayerUI(); // 시작 시 UI를 강제로 한 번 맞출거에요 -> CurrentHP 기반으로 + } // 코드 블록을 끝낼거에요 -> PlayerHealth 처리 - private void LateUpdate() - { - // 체력바가 항상 카메라를 바라보게 함 (빌보드 효과) - if (Camera.main != null) - transform.LookAt(transform.position + Camera.main.transform.forward); - } + // ✅ 나머지는 (current,max) Action 이벤트라 기존처럼 += 구독할거에요 -> 기존 코드 유지 + else if (healthSource is TrainingDummy td) td.OnHealthChanged += UpdateUI; // 더미면 (current,max) 이벤트 구독할거에요 -> UpdateUI로 + else if (healthSource is EnemyHealth eh) eh.OnHealthChanged += UpdateUI; // 적이면 (current,max) 이벤트 구독할거에요 -> UpdateUI로 + else if (healthSource is MonsterClass mc) mc.OnHealthChanged += UpdateUI; // 몬스터면 (current,max) 이벤트 구독할거에요 -> UpdateUI로 - private void OnDestroy() - { - // 이벤트 해제 (메모리 누수 방지) - if (healthSource is PlayerHealth ph) ph.OnHealthChanged -= UpdateUI; - else if (healthSource is TrainingDummy td) td.OnHealthChanged -= UpdateUI; - else if (healthSource is EnemyHealth eh) eh.OnHealthChanged -= UpdateUI; - else if (healthSource is MonsterClass mc) mc.OnHealthChanged -= UpdateUI; - } -} + if (hpFillImage != null) hpFillImage.fillAmount = 1f; // 조건이 맞으면 실행할거에요 -> 시작 시 게이지를 풀로 표시 + } // 코드 블록을 끝낼거에요 -> Start를 + + private void ForceRefreshPlayerUI() // 함수를 선언할거에요 -> PlayerHealth용 UI 강제 갱신 함수 ForceRefreshPlayerUI를 + { // 코드 블록을 시작할거에요 -> ForceRefreshPlayerUI 범위를 + if (_playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 없으면 + float max = (_playerStats != null) ? _playerStats.MaxHealth : 0f; // 값을 구할거에요 -> Stats가 있으면 MaxHealth를, 없으면 0을 + UpdateUI(_playerHealth.CurrentHP, max); // 함수를 실행할거에요 -> current/max 형태로 UI를 통일해서 갱신 + } // 코드 블록을 끝낼거에요 -> ForceRefreshPlayerUI를 + + private void UpdateUIFromRatio(float ratio) // 함수를 선언할거에요 -> PlayerHealth(UnityEvent)가 보내는 ratio를 받는 UpdateUIFromRatio를 + { // 코드 블록을 시작할거에요 -> UpdateUIFromRatio 범위를 + ratio = Mathf.Clamp01(ratio); // 값을 보정할거에요 -> ratio를 0~1로 고정 + if (hpFillImage != null) hpFillImage.fillAmount = ratio; // 조건이 맞으면 실행할거에요 -> 게이지를 ratio만큼 채우기 + + // 텍스트는 ratio만으론 current/max를 모름 -> PlayerHealth/Stats에서 직접 읽어서 찍을거에요 -> 코드 영향 최소 + if (_playerHealth != null && hpText != null) // 조건을 검사할거에요 -> PlayerHealth와 텍스트가 있는지 + { // 코드 블록을 시작할거에요 -> 텍스트 갱신 범위를 + float max = (_playerStats != null) ? _playerStats.MaxHealth : 0f; // 값을 구할거에요 -> Stats가 있으면 MaxHealth를 + float cur = _playerHealth.CurrentHP; // 값을 구할거에요 -> PlayerHealth의 현재 체력을 + hpText.text = (max > 0f) // 조건을 검사할거에요 -> max가 0보다 큰지 + ? $"{Mathf.CeilToInt(cur)} / {Mathf.CeilToInt(max)}" // 조건이 맞으면 실행할거에요 -> current/max로 표시 + : $"{Mathf.RoundToInt(ratio * 100f)}%"; // 조건이 틀리면 실행할거에요 -> max를 못 구하면 퍼센트로 표시 + } // 코드 블록을 끝낼거에요 -> 텍스트 갱신 + } // 코드 블록을 끝낼거에요 -> UpdateUIFromRatio를 + + private void UpdateUI(float current, float max) // 함수를 선언할거에요 -> (current,max) 이벤트용 UI 갱신 함수 UpdateUI를 + { // 코드 블록을 시작할거에요 -> UpdateUI 범위를 + if (hpFillImage != null && max > 0f) // 조건을 검사할거에요 -> 이미지가 있고 max가 0보다 큰지 + { // 코드 블록을 시작할거에요 -> 게이지 갱신 범위를 + hpFillImage.fillAmount = current / max; // 값을 넣을거에요 -> 체력 비율로 게이지 채우기 + } // 코드 블록을 끝낼거에요 -> 게이지 갱신 + + if (hpText != null) // 조건을 검사할거에요 -> 텍스트가 있는지 + { // 코드 블록을 시작할거에요 -> 텍스트 갱신 범위를 + hpText.text = $"{Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)}"; // 값을 넣을거에요 -> current/max 텍스트 표시 + } // 코드 블록을 끝낼거에요 -> 텍스트 갱신 + } // 코드 블록을 끝낼거에요 -> UpdateUI를 + + private void LateUpdate() // 함수를 선언할거에요 -> 모든 Update 후 실행되는 LateUpdate를 + { // 코드 블록을 시작할거에요 -> LateUpdate 범위를 + if (Camera.main != null) // 조건을 검사할거에요 -> 메인 카메라가 있는지 + transform.LookAt(transform.position + Camera.main.transform.forward); // 회전을 맞출거에요 -> 카메라 전방을 바라보게(빌보드) + } // 코드 블록을 끝낼거에요 -> LateUpdate를 + + private void OnDestroy() // 함수를 선언할거에요 -> 오브젝트가 파괴될 때 호출되는 OnDestroy를 + { // 코드 블록을 시작할거에요 -> OnDestroy 범위를 + + // ✅ PlayerHealth는 UnityEvent라 RemoveListener로 해제할거에요 -> 누수 방지 + if (_playerHealth != null) _playerHealth.OnHealthChanged.RemoveListener(UpdateUIFromRatio); // 조건이 맞으면 실행할거에요 -> UnityEvent 구독 해제 + + // ✅ 나머지는 Action 이벤트라 기존처럼 -= 해제할거에요 -> 기존 코드 유지 + if (healthSource is TrainingDummy td) td.OnHealthChanged -= UpdateUI; // 더미면 구독 해제할거에요 -> UpdateUI를 + else if (healthSource is EnemyHealth eh) eh.OnHealthChanged -= UpdateUI; // 적이면 구독 해제할거에요 -> UpdateUI를 + else if (healthSource is MonsterClass mc) mc.OnHealthChanged -= UpdateUI; // 몬스터면 구독 해제할거에요 -> UpdateUI를 + + } // 코드 블록을 끝낼거에요 -> OnDestroy를 + +} // 코드 블록을 끝낼거에요 -> HPUibar를 \ No newline at end of file diff --git a/Assets/Scripts/UI/HUD/Player Stat UI.cs b/Assets/Scripts/UI/HUD/Player Stat UI.cs index 5846671a..7009f62f 100644 --- a/Assets/Scripts/UI/HUD/Player Stat UI.cs +++ b/Assets/Scripts/UI/HUD/Player Stat UI.cs @@ -1,43 +1,43 @@ -using UnityEngine; -using TMPro; +using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 +using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를 -public class PlayerStatsUI : MonoBehaviour +public class PlayerStatsUI : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerStatsUI를 { - [Header("--- 데이터 연결 ---")] - [SerializeField] private Stats playerStats; + [Header("--- 데이터 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 데이터 연결 --- 을 + [SerializeField] private Stats playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트를 - [Header("--- UI 오브젝트 ---")] - [SerializeField] private GameObject statWindowPanel; + [Header("--- UI 오브젝트 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- UI 오브젝트 --- 를 + [SerializeField] private GameObject statWindowPanel; // 변수를 선언할거에요 -> 스탯 창 패널 오브젝트를 - [Header("--- 텍스트 UI ---")] - [SerializeField] private TextMeshProUGUI maxHealthText; + [Header("--- 텍스트 UI ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 텍스트 UI --- 를 + [SerializeField] private TextMeshProUGUI maxHealthText; // 변수를 선언할거에요 -> 최대 체력 텍스트 UI를 // [제거] strengthText 변수는 인스펙터에서 비워두거나 삭제하세요. - [SerializeField] private TextMeshProUGUI damageText; - [SerializeField] private TextMeshProUGUI speedText; + [SerializeField] private TextMeshProUGUI damageText; // 변수를 선언할거에요 -> 데미지 텍스트 UI를 + [SerializeField] private TextMeshProUGUI speedText; // 변수를 선언할거에요 -> 이동속도 텍스트 UI를 - private void Start() + private void Start() // 함수를 실행할거에요 -> 스크립트 시작 시 Start를 { - if (statWindowPanel != null) statWindowPanel.SetActive(false); + if (statWindowPanel != null) statWindowPanel.SetActive(false); // 조건이 맞으면 실행할거에요 -> 스탯 창이 있다면 처음에 끄기를 } - public void ToggleWindow() + public void ToggleWindow() // 함수를 선언할거에요 -> 스탯 창을 켜고 끄는 ToggleWindow를 { - if (statWindowPanel == null) return; - bool isActive = !statWindowPanel.activeSelf; - statWindowPanel.SetActive(isActive); - if (isActive) UpdateStatTexts(); + if (statWindowPanel == null) return; // 조건이 맞으면 중단할거에요 -> 패널이 연결되지 않았다면 + bool isActive = !statWindowPanel.activeSelf; // 값을 반전시킬거에요 -> 현재 활성화 상태를 뒤집어서 + statWindowPanel.SetActive(isActive); // 설정을 바꿀거에요 -> 패널의 활성화 상태를 + if (isActive) UpdateStatTexts(); // 조건이 맞으면 실행할거에요 -> 창이 켜졌다면 텍스트 갱신을 } - public void UpdateStatTexts() + public void UpdateStatTexts() // 함수를 선언할거에요 -> 스탯 텍스트들을 갱신하는 UpdateStatTexts를 { - if (playerStats == null) return; + if (playerStats == null) return; // 조건이 맞으면 중단할거에요 -> 스탯 데이터가 없다면 - maxHealthText.text = $"MaxHP: {playerStats.MaxHealth}"; + maxHealthText.text = $"MaxHP: {playerStats.MaxHealth}"; // 텍스트를 바꿀거에요 -> 최대 체력 수치로 // ✨ [수정] 힘 텍스트 업데이트 로직 삭제 - damageText.text = $"Damage: {playerStats.TotalAttackDamage} (+{playerStats.weaponDamage})"; + damageText.text = $"Damage: {playerStats.TotalAttackDamage} (+{playerStats.weaponDamage})"; // 텍스트를 바꿀거에요 -> 총 공격력과 무기 공격력으로 // ✨ [수정] 무게 페널티 없이 현재 속도만 깔끔하게 표기 - speedText.text = $"Speed: {playerStats.CurrentMoveSpeed:F1}"; + speedText.text = $"Speed: {playerStats.CurrentMoveSpeed:F1}"; // 텍스트를 바꿀거에요 -> 현재 이동 속도로 (소수점 1자리) } } \ No newline at end of file