From d21f0c9b6c6fcca3a71960ad656afaa340909188 Mon Sep 17 00:00:00 2001 From: Alexandre Coirier Date: Wed, 7 Oct 2026 15:46:51 +0200 Subject: [PATCH] feat: Player --- Assets/_Content/Data/Weapon.meta | 8 + .../_Content/Data/Weapon/WeaponData_Gun.asset | 20 ++ .../Data/Weapon/WeaponData_Gun.asset.meta | 8 + Assets/_Content/Models/Gun.fbx | 3 + Assets/_Content/Models/Gun.fbx.meta | 110 +++++++ Assets/_Content/Models/Projectile.fbx | 3 + Assets/_Content/Models/Projectile.fbx.meta | 110 +++++++ Assets/_Content/Prefabs/Player.prefab | 287 +++++++++++++++++- Assets/_Content/Prefabs/Projectile.prefab | 148 +++++++++ .../_Content/Prefabs/Projectile.prefab.meta | 7 + Assets/_Content/Scenes/SampleScene.unity | 4 +- Assets/_Content/Scripts/CameraFollower.cs | 13 - .../_Content/Scripts/CameraFollower.cs.meta | 2 - Assets/_Content/Scripts/Characters.meta | 8 + Assets/_Content/Scripts/Characters/Player.cs | 276 +++++++++++++++++ .../Player.cs.meta} | 0 .../Scripts/Characters/PlayerCamera.cs | 89 ++++++ .../Scripts/Characters/PlayerCamera.cs.meta | 2 + .../Scripts/Characters/PlayerEvent.cs | 96 ++++++ .../Scripts/Characters/PlayerEvent.cs.meta | 2 + Assets/_Content/Scripts/Combat.meta | 8 + Assets/_Content/Scripts/Combat/AmmoEffect.cs | 34 +++ .../Scripts/Combat/AmmoEffect.cs.meta | 2 + Assets/_Content/Scripts/Combat/HealEffect.cs | 34 +++ .../Scripts/Combat/HealEffect.cs.meta | 2 + Assets/_Content/Scripts/Combat/Health.cs | 83 +++++ Assets/_Content/Scripts/Combat/Health.cs.meta | 2 + Assets/_Content/Scripts/Combat/IDamageable.cs | 10 + .../Scripts/Combat/IDamageable.cs.meta | 2 + Assets/_Content/Scripts/Combat/Projectile.cs | 68 +++++ .../Scripts/Combat/Projectile.cs.meta | 2 + .../Scripts/Combat/ProjectileLauncher.cs | 107 +++++++ .../Scripts/Combat/ProjectileLauncher.cs.meta | 2 + Assets/_Content/Scripts/Combat/UiManager.cs | 68 +++++ .../_Content/Scripts/Combat/UiManager.cs.meta | 2 + Assets/_Content/Scripts/Combat/WeaponData.cs | 32 ++ .../Scripts/Combat/WeaponData.cs.meta | 2 + Assets/_Content/Scripts/PlayerMover.cs | 53 ---- Assets/_Content/Scripts/UI.meta | 8 + Assets/_Content/Scripts/UI/AmmoView.cs | 59 ++++ Assets/_Content/Scripts/UI/AmmoView.cs.meta | 2 + Assets/_Content/Scripts/UI/GameOverView.cs | 33 ++ .../_Content/Scripts/UI/GameOverView.cs.meta | 2 + Assets/_Content/Scripts/UI/HealthView.cs | 60 ++++ Assets/_Content/Scripts/UI/HealthView.cs.meta | 2 + 45 files changed, 1801 insertions(+), 74 deletions(-) create mode 100644 Assets/_Content/Data/Weapon.meta create mode 100644 Assets/_Content/Data/Weapon/WeaponData_Gun.asset create mode 100644 Assets/_Content/Data/Weapon/WeaponData_Gun.asset.meta create mode 100644 Assets/_Content/Models/Gun.fbx create mode 100644 Assets/_Content/Models/Gun.fbx.meta create mode 100644 Assets/_Content/Models/Projectile.fbx create mode 100644 Assets/_Content/Models/Projectile.fbx.meta create mode 100644 Assets/_Content/Prefabs/Projectile.prefab create mode 100644 Assets/_Content/Prefabs/Projectile.prefab.meta delete mode 100644 Assets/_Content/Scripts/CameraFollower.cs delete mode 100644 Assets/_Content/Scripts/CameraFollower.cs.meta create mode 100644 Assets/_Content/Scripts/Characters.meta create mode 100644 Assets/_Content/Scripts/Characters/Player.cs rename Assets/_Content/Scripts/{PlayerMover.cs.meta => Characters/Player.cs.meta} (100%) create mode 100644 Assets/_Content/Scripts/Characters/PlayerCamera.cs create mode 100644 Assets/_Content/Scripts/Characters/PlayerCamera.cs.meta create mode 100644 Assets/_Content/Scripts/Characters/PlayerEvent.cs create mode 100644 Assets/_Content/Scripts/Characters/PlayerEvent.cs.meta create mode 100644 Assets/_Content/Scripts/Combat.meta create mode 100644 Assets/_Content/Scripts/Combat/AmmoEffect.cs create mode 100644 Assets/_Content/Scripts/Combat/AmmoEffect.cs.meta create mode 100644 Assets/_Content/Scripts/Combat/HealEffect.cs create mode 100644 Assets/_Content/Scripts/Combat/HealEffect.cs.meta create mode 100644 Assets/_Content/Scripts/Combat/Health.cs create mode 100644 Assets/_Content/Scripts/Combat/Health.cs.meta create mode 100644 Assets/_Content/Scripts/Combat/IDamageable.cs create mode 100644 Assets/_Content/Scripts/Combat/IDamageable.cs.meta create mode 100644 Assets/_Content/Scripts/Combat/Projectile.cs create mode 100644 Assets/_Content/Scripts/Combat/Projectile.cs.meta create mode 100644 Assets/_Content/Scripts/Combat/ProjectileLauncher.cs create mode 100644 Assets/_Content/Scripts/Combat/ProjectileLauncher.cs.meta create mode 100644 Assets/_Content/Scripts/Combat/UiManager.cs create mode 100644 Assets/_Content/Scripts/Combat/UiManager.cs.meta create mode 100644 Assets/_Content/Scripts/Combat/WeaponData.cs create mode 100644 Assets/_Content/Scripts/Combat/WeaponData.cs.meta delete mode 100644 Assets/_Content/Scripts/PlayerMover.cs create mode 100644 Assets/_Content/Scripts/UI.meta create mode 100644 Assets/_Content/Scripts/UI/AmmoView.cs create mode 100644 Assets/_Content/Scripts/UI/AmmoView.cs.meta create mode 100644 Assets/_Content/Scripts/UI/GameOverView.cs create mode 100644 Assets/_Content/Scripts/UI/GameOverView.cs.meta create mode 100644 Assets/_Content/Scripts/UI/HealthView.cs create mode 100644 Assets/_Content/Scripts/UI/HealthView.cs.meta diff --git a/Assets/_Content/Data/Weapon.meta b/Assets/_Content/Data/Weapon.meta new file mode 100644 index 0000000..79c28f2 --- /dev/null +++ b/Assets/_Content/Data/Weapon.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7773c1d71eef8b3458b1f5d8870d968f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Content/Data/Weapon/WeaponData_Gun.asset b/Assets/_Content/Data/Weapon/WeaponData_Gun.asset new file mode 100644 index 0000000..09a663e --- /dev/null +++ b/Assets/_Content/Data/Weapon/WeaponData_Gun.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b4a4852c6f394a441935fae7b9f9ec82, type: 3} + m_Name: WeaponData_Gun + m_EditorClassIdentifier: Assembly-CSharp::Arena.Combat.WeaponData + _damage: 10 + _fireInterval: 0.2 + _maxAmmo: 30 + _projectilePrefab: {fileID: 1267975713900649858, guid: ab8f44a00f0019943847ff16fd95cdfd, type: 3} + _projectileSpeed: 20 + _projectileLifetime: 2 diff --git a/Assets/_Content/Data/Weapon/WeaponData_Gun.asset.meta b/Assets/_Content/Data/Weapon/WeaponData_Gun.asset.meta new file mode 100644 index 0000000..2a2c2b9 --- /dev/null +++ b/Assets/_Content/Data/Weapon/WeaponData_Gun.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c26b3ad646cf24c48904c1878f8d37e8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Content/Models/Gun.fbx b/Assets/_Content/Models/Gun.fbx new file mode 100644 index 0000000..658ad46 --- /dev/null +++ b/Assets/_Content/Models/Gun.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:840c484baff5f480cc4432726770198dd5752db548b7fa9aaee8d6b7d4734eb3 +size 16716 diff --git a/Assets/_Content/Models/Gun.fbx.meta b/Assets/_Content/Models/Gun.fbx.meta new file mode 100644 index 0000000..eb99037 --- /dev/null +++ b/Assets/_Content/Models/Gun.fbx.meta @@ -0,0 +1,110 @@ +fileFormatVersion: 2 +guid: 6fff3f1a06c6aab4fb5e1055f1d22848 +ModelImporter: + serializedVersion: 24200 + internalIDToNameTable: [] + externalObjects: {} + materials: + materialImportMode: 2 + materialName: 0 + materialSearch: 1 + materialLocation: 1 + animations: + legacyGenerateAnimations: 4 + bakeSimulation: 0 + resampleCurves: 1 + optimizeGameObjects: 0 + removeConstantScaleCurves: 0 + motionNodeName: + animationImportErrors: + animationImportWarnings: + animationRetargetingWarnings: + animationDoRetargetingWarnings: 0 + importAnimatedCustomProperties: 0 + importConstraints: 0 + animationCompression: 1 + animationRotationError: 0.5 + animationPositionError: 0.5 + animationScaleError: 0.5 + animationWrapMode: 0 + extraExposedTransformPaths: [] + extraUserProperties: [] + clipAnimations: [] + isReadable: 0 + meshes: + lODScreenPercentages: [] + globalScale: 100 + 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 + generateMeshLods: 0 + meshLodGenerationFlags: 0 + maximumMeshLod: -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: 0 + skeletonHasParents: 1 + lastHumanDescriptionAvatarSource: {instanceID: 0} + autoGenerateAvatarMappingIfUnspecified: 1 + animationType: 2 + humanoidOversampling: 1 + avatarSetup: 0 + addHumanoidExtraRootOnlyWhenUsingAvatar: 1 + importBlendShapeDeformPercent: 1 + remapMaterialsIfMaterialImportModeIsNone: 0 + additionalBone: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Content/Models/Projectile.fbx b/Assets/_Content/Models/Projectile.fbx new file mode 100644 index 0000000..168d3b5 --- /dev/null +++ b/Assets/_Content/Models/Projectile.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:890af87fbd01bae150144343d530b73aa4c6eda3ff3ff56fffcdb69bd02f1540 +size 16108 diff --git a/Assets/_Content/Models/Projectile.fbx.meta b/Assets/_Content/Models/Projectile.fbx.meta new file mode 100644 index 0000000..5d0ba69 --- /dev/null +++ b/Assets/_Content/Models/Projectile.fbx.meta @@ -0,0 +1,110 @@ +fileFormatVersion: 2 +guid: 353aa13c4cf685d4da0564413baffc19 +ModelImporter: + serializedVersion: 24200 + internalIDToNameTable: [] + externalObjects: {} + materials: + materialImportMode: 2 + materialName: 0 + materialSearch: 1 + materialLocation: 1 + animations: + legacyGenerateAnimations: 4 + bakeSimulation: 0 + resampleCurves: 1 + optimizeGameObjects: 0 + removeConstantScaleCurves: 0 + motionNodeName: + animationImportErrors: + animationImportWarnings: + animationRetargetingWarnings: + animationDoRetargetingWarnings: 0 + importAnimatedCustomProperties: 0 + importConstraints: 0 + animationCompression: 1 + animationRotationError: 0.5 + animationPositionError: 0.5 + animationScaleError: 0.5 + animationWrapMode: 0 + extraExposedTransformPaths: [] + extraUserProperties: [] + clipAnimations: [] + isReadable: 0 + meshes: + lODScreenPercentages: [] + globalScale: 100 + 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 + generateMeshLods: 0 + meshLodGenerationFlags: 0 + maximumMeshLod: -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: 0 + skeletonHasParents: 1 + lastHumanDescriptionAvatarSource: {instanceID: 0} + autoGenerateAvatarMappingIfUnspecified: 1 + animationType: 2 + humanoidOversampling: 1 + avatarSetup: 0 + addHumanoidExtraRootOnlyWhenUsingAvatar: 1 + importBlendShapeDeformPercent: 1 + remapMaterialsIfMaterialImportModeIsNone: 0 + additionalBone: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Content/Prefabs/Player.prefab b/Assets/_Content/Prefabs/Player.prefab index 4bd8c74..ea2d0c9 100644 --- a/Assets/_Content/Prefabs/Player.prefab +++ b/Assets/_Content/Prefabs/Player.prefab @@ -1,5 +1,126 @@ %YAML 1.1 %TAG !u! tag:unity3d.com,2011: +--- !u!1 &3321106030899012940 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 635898796213162980} + m_Layer: 0 + m_Name: Muzzle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &635898796213162980 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3321106030899012940} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0.064, z: 0.446} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4595290834041894522} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &5286650573156461405 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2648838692056603215} + - component: {fileID: 4433368355904579625} + - component: {fileID: 7841523364635298371} + m_Layer: 0 + m_Name: Cube + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2648838692056603215 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5286650573156461405} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0.413, z: 0.46} + m_LocalScale: {x: 0.57788104, y: 0.23092704, z: 0.3382222} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8656262745496025766} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &4433368355904579625 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5286650573156461405} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &7841523364635298371 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5286650573156461405} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &5701756709460610233 GameObject: m_ObjectHideFlags: 0 @@ -9,6 +130,8 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 1336261255310037562} + - component: {fileID: 4700255994880078248} + - component: {fileID: 7678313450534884141} m_Layer: 0 m_Name: Player m_TagString: Untagged @@ -30,8 +153,60 @@ Transform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 8656262745496025766} + - {fileID: 4595290834041894522} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!143 &4700255994880078248 +CharacterController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5701756709460610233} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Height: 1.64 + m_Radius: 0.37 + m_SlopeLimit: 45 + m_StepOffset: 0.3 + m_SkinWidth: 0.08 + m_MinMoveDistance: 0.001 + m_Center: {x: 0, y: 0.9, z: 0} +--- !u!114 &7678313450534884141 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5701756709460610233} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 502392351c6ed7e4c9a3a895792f5b68, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::PlayerMovero + _settings: + Speed: 10 + _references: + Controller: {fileID: 4700255994880078248} + Launcher: {fileID: 6426658406708130876} + MoveAction: {fileID: -1680190386980627800, guid: 052faaac586de48259a63d0c4782560b, type: 3} + LookAction: {fileID: -5630151704836100654, guid: 052faaac586de48259a63d0c4782560b, type: 3} + AttackAction: {fileID: 1120369429361536294, guid: 052faaac586de48259a63d0c4782560b, type: 3} + InteractAction: {fileID: 1781555164194001046, guid: 052faaac586de48259a63d0c4782560b, type: 3} + _state: + IsMoving: 0 + _health: + Max: 100 + _current: 0 --- !u!1 &7176853314782784158 GameObject: m_ObjectHideFlags: 0 @@ -59,10 +234,11 @@ Transform: m_GameObject: {fileID: 7176853314782784158} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 1, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] + m_LocalPosition: {x: 0, y: 0.9, z: 0} + m_LocalScale: {x: 0.9, y: 0.9, z: 0.9} + m_ConstrainProportionsScale: 1 + m_Children: + - {fileID: 2648838692056603215} m_Father: {fileID: 1336261255310037562} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!33 &179238054112632900 @@ -122,3 +298,106 @@ MeshRenderer: m_SortingOrder: 0 m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} +--- !u!1001 &4057595757166956945 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 1336261255310037562} + m_Modifications: + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalScale.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalScale.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalScale.z + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalPosition.x + value: 0.479 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalPosition.y + value: 0.878 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalPosition.z + value: 0.404 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 919132149155446097, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + propertyPath: m_Name + value: Gun + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: + - targetCorrespondingSourceObject: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + insertIndex: -1 + addedObject: {fileID: 635898796213162980} + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + insertIndex: -1 + addedObject: {fileID: 6426658406708130876} + m_SourcePrefab: {fileID: 100100000, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} +--- !u!1 &3786981955428199616 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + m_PrefabInstance: {fileID: 4057595757166956945} + m_PrefabAsset: {fileID: 0} +--- !u!114 &6426658406708130876 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3786981955428199616} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1a1a8c56cfabcd648a0967ce7acc5ed3, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::Arena.Combat.ProjectileLauncher + _settings: + Weapon: {fileID: 11400000, guid: c26b3ad646cf24c48904c1878f8d37e8, type: 2} + _references: + Muzzle: {fileID: 635898796213162980} + _state: + Ammo: 0 +--- !u!4 &4595290834041894522 stripped +Transform: + m_CorrespondingSourceObject: {fileID: -8679921383154817045, guid: 6fff3f1a06c6aab4fb5e1055f1d22848, type: 3} + m_PrefabInstance: {fileID: 4057595757166956945} + m_PrefabAsset: {fileID: 0} diff --git a/Assets/_Content/Prefabs/Projectile.prefab b/Assets/_Content/Prefabs/Projectile.prefab new file mode 100644 index 0000000..b062da8 --- /dev/null +++ b/Assets/_Content/Prefabs/Projectile.prefab @@ -0,0 +1,148 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1001 &6417798391929613390 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalScale.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalScale.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalScale.z + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 919132149155446097, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + propertyPath: m_Name + value: Projectile + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + insertIndex: -1 + addedObject: {fileID: 367274961135860047} + - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + insertIndex: -1 + addedObject: {fileID: 976289565335777750} + - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + insertIndex: -1 + addedObject: {fileID: 1267975713900649858} + m_SourcePrefab: {fileID: 100100000, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} +--- !u!1 &6183988142400639263 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 353aa13c4cf685d4da0564413baffc19, type: 3} + m_PrefabInstance: {fileID: 6417798391929613390} + m_PrefabAsset: {fileID: 0} +--- !u!135 &367274961135860047 +SphereCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6183988142400639263} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Radius: 0.3 + m_Center: {x: 0, y: 0, z: 0} +--- !u!54 &976289565335777750 +Rigidbody: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6183988142400639263} + serializedVersion: 5 + m_Mass: 1 + m_LinearDamping: 0 + m_AngularDamping: 0.05 + m_CenterOfMass: {x: 0, y: 0, z: 0} + m_InertiaTensor: {x: 1, y: 1, z: 1} + m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ImplicitCom: 1 + m_ImplicitTensor: 1 + m_UseGravity: 1 + m_IsKinematic: 0 + m_Interpolate: 0 + m_Constraints: 0 + m_CollisionDetection: 0 +--- !u!114 &1267975713900649858 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6183988142400639263} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7671f358930703f4683397845e17e460, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::Arena.Combat.Projectile + _settings: + Speed: 20 + Lifetime: 2 diff --git a/Assets/_Content/Prefabs/Projectile.prefab.meta b/Assets/_Content/Prefabs/Projectile.prefab.meta new file mode 100644 index 0000000..8bdf21d --- /dev/null +++ b/Assets/_Content/Prefabs/Projectile.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ab8f44a00f0019943847ff16fd95cdfd +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Content/Scenes/SampleScene.unity b/Assets/_Content/Scenes/SampleScene.unity index f2ceb8c..0ef9dcd 100644 --- a/Assets/_Content/Scenes/SampleScene.unity +++ b/Assets/_Content/Scenes/SampleScene.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7308cb7ab832f786810414a0de37ddafc469ca50dc16d5af2d312d79df4d2a5 -size 21305 +oid sha256:39b1d70d8b5e42aea6c4d62702d3b147b6f2c070bce573df6592d0ff99469161 +size 21229 diff --git a/Assets/_Content/Scripts/CameraFollower.cs b/Assets/_Content/Scripts/CameraFollower.cs deleted file mode 100644 index 2c0c22a..0000000 --- a/Assets/_Content/Scripts/CameraFollower.cs +++ /dev/null @@ -1,13 +0,0 @@ -using UnityEngine; - -[ExecuteAlways] -public class CameraFollower : MonoBehaviour -{ - public GameObject Player; - - void Update() - { - if (Player) - transform.position = Player.transform.position; - } -} diff --git a/Assets/_Content/Scripts/CameraFollower.cs.meta b/Assets/_Content/Scripts/CameraFollower.cs.meta deleted file mode 100644 index a54b07e..0000000 --- a/Assets/_Content/Scripts/CameraFollower.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 4ae4fbdc595247448b3d604825bd07fe \ No newline at end of file diff --git a/Assets/_Content/Scripts/Characters.meta b/Assets/_Content/Scripts/Characters.meta new file mode 100644 index 0000000..e8ceff7 --- /dev/null +++ b/Assets/_Content/Scripts/Characters.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 00d5f0f5167a1a243a9972a98593b122 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Content/Scripts/Characters/Player.cs b/Assets/_Content/Scripts/Characters/Player.cs new file mode 100644 index 0000000..0979067 --- /dev/null +++ b/Assets/_Content/Scripts/Characters/Player.cs @@ -0,0 +1,276 @@ +using System; +using UnityEngine; +using UnityEngine.InputSystem; +using Arena.Combat; + +namespace Arena.Characters +{ + public class Player : MonoBehaviour, IDamageable + { + [System.Serializable] + public class Settings + { + [Tooltip("Movement speed, in km/h.")] + [Min(0f)] public float Speed = 5f; + } + + [System.Serializable] + public struct References + { + public CharacterController Controller; + public ProjectileLauncher Launcher; + public InputActionReference MoveAction; + public InputActionReference LookAction; + public InputActionReference AttackAction; + public InputActionReference InteractAction; + } + + [System.Serializable] + public class StateContainer + { + public bool IsMoving; + } + + #region Fields + + private const float KMH_TO_MS = 1f / 3.6f; + private const float AIM_STICK_DEADZONE = 0.2f; + + [SerializeField] private Settings _settings; + [SerializeField] private References _references; + [SerializeField] private StateContainer _state; + [SerializeField] private Health _health; + + /// + /// Raised when the interact action is performed. + /// + public event Action OnInteract; + + /// + /// The only player of the scene. + /// + public static Player Instance { get; private set; } + public StateContainer State => _state; + public Health Health => _health; + public ProjectileLauncher Launcher => _references.Launcher; + + private Camera _camera; + private Vector2 _moveInput; + private Vector2 _aimStickInput; + private bool _isAimingWithStick; + private bool _isAttacking; + private bool _isInteracting; + private Vector3 _aimDirection = Vector3.forward; + + #endregion + + #region Unity Lifecycle + + void Awake() + { + // Only one player can exist: a second one is removed + if (Instance && Instance != this) + { + Debug.LogWarning($"A second Player was found on {name}, it is destroyed."); + Destroy(gameObject); + return; + } + + Instance = this; + + // Camera.main is the camera tagged MainCamera + _camera = Camera.main; + _health.Initialize(); + } + + void OnEnable() + { + SetInputsEnabled(true); + } + + void OnDisable() + { + SetInputsEnabled(false); + } + + void OnDestroy() + { + if (Instance == this) + Instance = null; + } + + void Update() + { + // A dead player no longer moves nor shoots + if (_health.IsDead) + return; + + float deltaTime = Time.deltaTime; + + GetInputs(); + Move(deltaTime); + Aim(); + Attack(); + Interact(); + } + + #endregion + + #region Public Methods + + public void TakeDamage(int amount) + { + _health.TakeDamage(amount); + } + + /// + /// Teleports the player. + /// + /// x, y, z: position. w: angle around the vertical axis, in degrees. + public void Teleport(Vector4 pose) + { + // An enabled CharacterController would overwrite the new position + _references.Controller.enabled = false; + + Vector3 position = new Vector3(pose.x, pose.y, pose.z); + Quaternion rotation = Quaternion.Euler(0f, pose.w, 0f); + transform.SetPositionAndRotation(position, rotation); + + _references.Controller.enabled = true; + } + + /// + /// Teleports the player to the position and heading of a target, such as a spawn point. + /// + public void Teleport(Transform target) + { + Vector3 position = target.position; + Vector4 pose = new Vector4(position.x, position.y, position.z, target.eulerAngles.y); + Teleport(pose); + } + + #endregion + + #region Private Methods + + private void SetInputsEnabled(bool isEnabled) + { + InputActionReference[] actions = + { + _references.MoveAction, + _references.LookAction, + _references.AttackAction, + _references.InteractAction + }; + + foreach (InputActionReference action in actions) + { + if (isEnabled) + action.action.Enable(); + else + action.action.Disable(); + } + } + + private void GetInputs() + { + _moveInput = _references.MoveAction.action.ReadValue(); + _isAttacking = _references.AttackAction.action.IsPressed(); + _isInteracting = _references.InteractAction.action.WasPerformedThisFrame(); + + InputAction look = _references.LookAction.action; + _aimStickInput = look.ReadValue(); + + // Look is also bound to the mouse delta: only the gamepad counts as a stick + bool isStick = look.activeControl != null && look.activeControl.device is Gamepad; + + // The last aiming device used wins: the stick when pushed, the mouse when moved + if (isStick && IsStickPushed()) + _isAimingWithStick = true; + else if (Pointer.current != null && Pointer.current.delta.ReadValue().sqrMagnitude > 0f) + _isAimingWithStick = false; + } + + private void Move(float deltaTime) + { + Vector3 direction = ToWorldDirection(_moveInput); + _state.IsMoving = direction.sqrMagnitude > 0f; + + Vector3 velocity = direction * _settings.Speed * KMH_TO_MS; + _references.Controller.Move(velocity * deltaTime); + } + + private void Aim() + { + if (_isAimingWithStick) + { + // Released stick: the player keeps its last direction + if (IsStickPushed()) + _aimDirection = ToWorldDirection(_aimStickInput); + } + else if (TryGetPointerDirection(out Vector3 direction)) + { + _aimDirection = direction; + } + + transform.rotation = Quaternion.LookRotation(_aimDirection); + } + + private void Attack() + { + if (_isAttacking) + _references.Launcher.TryFire(); + } + + private void Interact() + { + if (_isInteracting) + OnInteract?.Invoke(); + } + + private bool IsStickPushed() + { + return _aimStickInput.sqrMagnitude > AIM_STICK_DEADZONE * AIM_STICK_DEADZONE; + } + + /// + /// Converts a 2D input into a world direction on the ground, relative to the camera heading. + /// + private Vector3 ToWorldDirection(Vector2 input) + { + // Only the camera rotation around the vertical axis is kept + Quaternion cameraHeading = Quaternion.Euler(0f, _camera.transform.eulerAngles.y, 0f); + return cameraHeading * new Vector3(input.x, 0f, input.y); + } + + /// + /// Direction from the player to the point under the pointer, on a horizontal plane at the player's height. + /// + private bool TryGetPointerDirection(out Vector3 direction) + { + direction = Vector3.zero; + + if (Pointer.current == null) + return false; + + // Mouse position read directly: the Look action only gives its movement + Ray ray = _camera.ScreenPointToRay(Pointer.current.position.ReadValue()); + Plane ground = new Plane(Vector3.up, transform.position); + + if (!ground.Raycast(ray, out float distance)) + return false; + + direction = ray.GetPoint(distance) - transform.position; + direction.y = 0f; + return direction.sqrMagnitude > 0.01f; + } + + [ContextMenu("Debug/Take 25 Damage")] + private void DebugTakeDamage() + { + TakeDamage(25); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/PlayerMover.cs.meta b/Assets/_Content/Scripts/Characters/Player.cs.meta similarity index 100% rename from Assets/_Content/Scripts/PlayerMover.cs.meta rename to Assets/_Content/Scripts/Characters/Player.cs.meta diff --git a/Assets/_Content/Scripts/Characters/PlayerCamera.cs b/Assets/_Content/Scripts/Characters/PlayerCamera.cs new file mode 100644 index 0000000..88d80b1 --- /dev/null +++ b/Assets/_Content/Scripts/Characters/PlayerCamera.cs @@ -0,0 +1,89 @@ +using UnityEngine; + +namespace Arena.Characters +{ + [ExecuteAlways] + public class PlayerCamera : MonoBehaviour + { + [System.Serializable] + public class Settings + { + [Tooltip("x: horizontal distance from the player, y: height above the player, in meters.")] + public Vector2 Offset = new Vector2(8f, 12f); + + [Tooltip("Rotation of the offset around the player, in degrees.")] + public float Angle = 0f; + + [Tooltip("Time to catch up with the player, in seconds. 0: no smoothing.")] + [Range(0f, 1f)] public float Smooth = 0.2f; + } + + [System.Serializable] + public struct References + { + [Tooltip("Followed player. Found automatically when empty.")] + public Player Player; + } + + #region Fields + + [SerializeField] private Settings _settings; + [SerializeField] private References _references; + + private Vector3 _velocity; + + #endregion + + #region Unity Lifecycle + + // LateUpdate: the player has already moved this frame + void LateUpdate() + { + if (!GetPlayer()) + return; + + Vector3 targetPos = GetTargetPosition(); + + // No smoothing in edit mode: the camera snaps while the scene is edited + if (!Application.isPlaying || _settings.Smooth <= 0f) + { + transform.position = targetPos; + _velocity = Vector3.zero; + } + else + { + transform.position = Vector3.SmoothDamp(transform.position, targetPos, ref _velocity, _settings.Smooth); + } + } + + #endregion + + #region Private Methods + + /// + /// Looks for the player: singleton first, then a search in the scene. Returns false if none is found. + /// + private bool GetPlayer() + { + if (_references.Player) + return true; + + // In edit mode, Player.Awake has not run, so the singleton is empty + else if (Player.Instance) + _references.Player = Player.Instance; + else + _references.Player = FindFirstObjectByType(); + + return _references.Player != null; + } + + private Vector3 GetTargetPosition() + { + Quaternion rotation = Quaternion.Euler(0f, _settings.Angle, 0f); + Vector3 offset = rotation * new Vector3(0f, _settings.Offset.y, -_settings.Offset.x); + return _references.Player.transform.position + offset; + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/Characters/PlayerCamera.cs.meta b/Assets/_Content/Scripts/Characters/PlayerCamera.cs.meta new file mode 100644 index 0000000..8c07fab --- /dev/null +++ b/Assets/_Content/Scripts/Characters/PlayerCamera.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bd78e931d0126914e8206e9f6bb2b9de \ No newline at end of file diff --git a/Assets/_Content/Scripts/Characters/PlayerEvent.cs b/Assets/_Content/Scripts/Characters/PlayerEvent.cs new file mode 100644 index 0000000..57a7709 --- /dev/null +++ b/Assets/_Content/Scripts/Characters/PlayerEvent.cs @@ -0,0 +1,96 @@ +using UnityEngine; +using UnityEngine.Events; +using Arena.Characters; + +namespace Arena.Interactions +{ + /// + /// Zone that raises OnPlayer when the player enters it, or when the player interacts inside it. + /// It never acts on the player itself: the actions are set up in the Inspector. + /// + [RequireComponent(typeof(Collider))] + public class PlayerEvent : MonoBehaviour + { + public enum Trigger + { + OnPlayerEnter, + OnPlayerInteract + } + + [System.Serializable] + public class Settings + { + [Tooltip("OnPlayerEnter: as soon as the player enters. OnPlayerInteract: when the player interacts inside the zone.")] + public Trigger Trigger; + } + + #region Fields + + [SerializeField] private Settings _settings; + + [Tooltip("Actions raised for the player: heal, ammo, sound, hide this object...")] + [SerializeField] private UnityEvent _onPlayer; + + private bool _isListening; + + #endregion + + #region Unity Lifecycle + + private void OnDisable() + { + // The object can be hidden while the player is inside + StopListening(); + } + + private void OnTriggerEnter(Collider other) + { + if (!IsPlayer(other)) + return; + + if (_settings.Trigger == Trigger.OnPlayerEnter) + _onPlayer.Invoke(); + else + StartListening(); + } + + private void OnTriggerExit(Collider other) + { + if (IsPlayer(other)) + StopListening(); + } + + #endregion + + #region Private Methods + + private bool IsPlayer(Collider other) + { + return Player.Instance != null && other.gameObject == Player.Instance.gameObject; + } + + private void StartListening() + { + Player.Instance.OnInteract += HandleInteract; + _isListening = true; + } + + private void StopListening() + { + if (!_isListening) + return; + + if (Player.Instance != null) + Player.Instance.OnInteract -= HandleInteract; + + _isListening = false; + } + + private void HandleInteract() + { + _onPlayer.Invoke(); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/Characters/PlayerEvent.cs.meta b/Assets/_Content/Scripts/Characters/PlayerEvent.cs.meta new file mode 100644 index 0000000..5ad767d --- /dev/null +++ b/Assets/_Content/Scripts/Characters/PlayerEvent.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d3c874cb9bc4c364d87e100aada2b434 \ No newline at end of file diff --git a/Assets/_Content/Scripts/Combat.meta b/Assets/_Content/Scripts/Combat.meta new file mode 100644 index 0000000..251db50 --- /dev/null +++ b/Assets/_Content/Scripts/Combat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d5ab3689e81574b4c8d1b37201093edf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Content/Scripts/Combat/AmmoEffect.cs b/Assets/_Content/Scripts/Combat/AmmoEffect.cs new file mode 100644 index 0000000..b052340 --- /dev/null +++ b/Assets/_Content/Scripts/Combat/AmmoEffect.cs @@ -0,0 +1,34 @@ +using UnityEngine; +using Arena.Characters; + +namespace Arena.Interactions +{ + /// + /// Gives ammo to the player. Apply is meant to be linked to a PlayerEvent. + /// + public class AmmoEffect : MonoBehaviour + { + [System.Serializable] + public class Settings + { + [Tooltip("Ammo given to the player.")] + [Min(1)] public int Amount = 10; + } + + #region Fields + + [SerializeField] private Settings _settings; + + #endregion + + #region Public Methods + + public void Apply() + { + if (Player.Instance != null) + Player.Instance.Launcher.AddAmmo(_settings.Amount); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/Combat/AmmoEffect.cs.meta b/Assets/_Content/Scripts/Combat/AmmoEffect.cs.meta new file mode 100644 index 0000000..405554b --- /dev/null +++ b/Assets/_Content/Scripts/Combat/AmmoEffect.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3831749972a75dc4dbe47f8a551785fc \ No newline at end of file diff --git a/Assets/_Content/Scripts/Combat/HealEffect.cs b/Assets/_Content/Scripts/Combat/HealEffect.cs new file mode 100644 index 0000000..7725d69 --- /dev/null +++ b/Assets/_Content/Scripts/Combat/HealEffect.cs @@ -0,0 +1,34 @@ +using UnityEngine; +using Arena.Characters; + +namespace Arena.Interactions +{ + /// + /// Heals the player. Apply is meant to be linked to a PlayerEvent. + /// + public class HealEffect : MonoBehaviour + { + [System.Serializable] + public class Settings + { + [Tooltip("Hit points given to the player.")] + [Min(1)] public int Amount = 25; + } + + #region Fields + + [SerializeField] private Settings _settings; + + #endregion + + #region Public Methods + + public void Apply() + { + if (Player.Instance != null) + Player.Instance.Health.Heal(_settings.Amount); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/Combat/HealEffect.cs.meta b/Assets/_Content/Scripts/Combat/HealEffect.cs.meta new file mode 100644 index 0000000..1a9eaa7 --- /dev/null +++ b/Assets/_Content/Scripts/Combat/HealEffect.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3b9353836241da04e9bb64cc87f89745 \ No newline at end of file diff --git a/Assets/_Content/Scripts/Combat/Health.cs b/Assets/_Content/Scripts/Combat/Health.cs new file mode 100644 index 0000000..0dc484d --- /dev/null +++ b/Assets/_Content/Scripts/Combat/Health.cs @@ -0,0 +1,83 @@ +using System; +using UnityEngine; + +namespace Arena.Combat +{ + /// + /// Hit points of an entity. Serialized inside the component that owns it (Player, Enemy), + /// which calls Initialize in its Awake. + /// + [System.Serializable] + public class Health + { + #region Fields + + [Tooltip("Hit points when full.")] + [Min(1)] public int Max = 100; + + [Tooltip("Current hit points, visible for debugging.")] + [SerializeField] private int _current; + + /// + /// Raised with the current and max hit points each time they change. + /// + public event Action OnChange; + + /// + /// Raised once, when the hit points reach zero. + /// + public event Action OnDied; + + public int Current => _current; + public bool IsDead => _current <= 0; + + #endregion + + #region Public Methods + + /// + /// Fills the hit points. Called by the owner in its Awake. + /// + public void Initialize() + { + SetCurrent(Max); + } + + /// + /// Removes hit points and raises OnDied the first time they reach zero. + /// + public void TakeDamage(int amount) + { + if (IsDead || amount <= 0) + return; + + SetCurrent(_current - amount); + + if (IsDead) + OnDied?.Invoke(); + } + + /// + /// Gives hit points back without exceeding Max. Has no effect once dead. + /// + public void Heal(int amount) + { + if (IsDead || amount <= 0) + return; + + SetCurrent(_current + amount); + } + + #endregion + + #region Private Methods + + private void SetCurrent(int value) + { + _current = Mathf.Clamp(value, 0, Max); + OnChange?.Invoke(_current, Max); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/Combat/Health.cs.meta b/Assets/_Content/Scripts/Combat/Health.cs.meta new file mode 100644 index 0000000..7767857 --- /dev/null +++ b/Assets/_Content/Scripts/Combat/Health.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 220d6096ae28c12449f72c38860cbc2e \ No newline at end of file diff --git a/Assets/_Content/Scripts/Combat/IDamageable.cs b/Assets/_Content/Scripts/Combat/IDamageable.cs new file mode 100644 index 0000000..728e602 --- /dev/null +++ b/Assets/_Content/Scripts/Combat/IDamageable.cs @@ -0,0 +1,10 @@ +namespace Arena.Combat +{ + /// + /// Anything that can receive damage: player, enemy, destructible object. + /// + public interface IDamageable + { + void TakeDamage(int amount); + } +} diff --git a/Assets/_Content/Scripts/Combat/IDamageable.cs.meta b/Assets/_Content/Scripts/Combat/IDamageable.cs.meta new file mode 100644 index 0000000..734341b --- /dev/null +++ b/Assets/_Content/Scripts/Combat/IDamageable.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 14df4410feff3d044afb4ec552a18997 \ No newline at end of file diff --git a/Assets/_Content/Scripts/Combat/Projectile.cs b/Assets/_Content/Scripts/Combat/Projectile.cs new file mode 100644 index 0000000..d1ed2cf --- /dev/null +++ b/Assets/_Content/Scripts/Combat/Projectile.cs @@ -0,0 +1,68 @@ +using UnityEngine; + +namespace Arena.Combat +{ + /// + /// Moves straight ahead and damages the first IDamageable it touches. + /// Needs a trigger collider and a kinematic Rigidbody. + /// + [RequireComponent(typeof(Rigidbody))] + public class Projectile : MonoBehaviour + { + [System.Serializable] + public class Settings + { + [Tooltip("Speed, in meters per second.")] + [Min(0f)] public float Speed = 20f; + + [Tooltip("Time before an unused projectile is destroyed, in seconds.")] + [Min(0.1f)] public float Lifetime = 2f; + } + + #region Fields + + [SerializeField] private Settings _settings; + + private int _damage; + private GameObject _owner; + + #endregion + + #region Unity Lifecycle + + private void Update() + { + transform.position += transform.forward * _settings.Speed * Time.deltaTime; + } + + private void OnTriggerEnter(Collider other) + { + // Ignore the shooter and the other trigger zones + if (other.isTrigger || other.gameObject == _owner) + return; + + if (other.TryGetComponent(out IDamageable target)) + target.TakeDamage(_damage); + + Destroy(gameObject); + } + + #endregion + + #region Public Methods + + /// + /// Starts the projectile. Called right after Instantiate by whoever fires it. + /// + /// Damage dealt to the IDamageable it touches. + /// Shooter, ignored by the projectile. + public void Launch(int damage, GameObject owner) + { + _damage = damage; + _owner = owner; + Destroy(gameObject, _settings.Lifetime); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/Combat/Projectile.cs.meta b/Assets/_Content/Scripts/Combat/Projectile.cs.meta new file mode 100644 index 0000000..a99ffd2 --- /dev/null +++ b/Assets/_Content/Scripts/Combat/Projectile.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7671f358930703f4683397845e17e460 \ No newline at end of file diff --git a/Assets/_Content/Scripts/Combat/ProjectileLauncher.cs b/Assets/_Content/Scripts/Combat/ProjectileLauncher.cs new file mode 100644 index 0000000..acdda13 --- /dev/null +++ b/Assets/_Content/Scripts/Combat/ProjectileLauncher.cs @@ -0,0 +1,107 @@ +using System; +using UnityEngine; + +namespace Arena.Combat +{ + /// + /// Fires projectiles from a muzzle, limited by the fire interval and the ammo count. + /// Placed on the root of the shooter, which the projectiles ignore. + /// + public class ProjectileLauncher : MonoBehaviour + { + [System.Serializable] + public class Settings + { + [Tooltip("Weapon data: damage, fire interval, ammo, projectile.")] + public WeaponData Weapon; + } + + [System.Serializable] + public struct References + { + [Tooltip("Spawn point of the projectiles. Its forward axis gives the direction.")] + public Transform Muzzle; + } + + [System.Serializable] + public class StateContainer + { + public int Ammo; + } + + #region Fields + + [SerializeField] private Settings _settings; + [SerializeField] private References _references; + [SerializeField] private StateContainer _state; + + /// + /// Raised with the current and max ammo each time the ammo changes. + /// + public event Action OnAmmoChange; + + public StateContainer State => _state; + public int MaxAmmo => _settings.Weapon.MaxAmmo; + + private float _nextFireTime; + + #endregion + + #region Unity Lifecycle + + private void Awake() + { + _state.Ammo = MaxAmmo; + } + + #endregion + + #region Public Methods + + /// + /// Fires one projectile if the weapon is ready and has ammo. Returns true if a shot was fired. + /// + public bool TryFire() + { + if (Time.time < _nextFireTime || _state.Ammo <= 0) + return false; + + _nextFireTime = Time.time + _settings.Weapon.FireInterval; + SetAmmo(_state.Ammo - 1); + + Transform muzzle = _references.Muzzle; + Projectile projectile = Instantiate(_settings.Weapon.ProjectilePrefab, muzzle.position, muzzle.rotation); + projectile.Launch(_settings.Weapon.Damage, gameObject); + return true; + } + + /// + /// Adds ammo without exceeding the weapon maximum. + /// + public void AddAmmo(int amount) + { + if (amount <= 0) + return; + + SetAmmo(Mathf.Min(_state.Ammo + amount, MaxAmmo)); + } + + #endregion + + #region Private Methods + + private void SetAmmo(int value) + { + _state.Ammo = value; + OnAmmoChange?.Invoke(_state.Ammo, MaxAmmo); + } + + [ContextMenu("Debug/Try Fire")] + private void DebugTryFire() + { + TryFire(); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/Combat/ProjectileLauncher.cs.meta b/Assets/_Content/Scripts/Combat/ProjectileLauncher.cs.meta new file mode 100644 index 0000000..59e42f4 --- /dev/null +++ b/Assets/_Content/Scripts/Combat/ProjectileLauncher.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1a1a8c56cfabcd648a0967ce7acc5ed3 \ No newline at end of file diff --git a/Assets/_Content/Scripts/Combat/UiManager.cs b/Assets/_Content/Scripts/Combat/UiManager.cs new file mode 100644 index 0000000..dd6f3d7 --- /dev/null +++ b/Assets/_Content/Scripts/Combat/UiManager.cs @@ -0,0 +1,68 @@ +using UnityEngine; +using Arena.Characters; + +namespace Arena.UI +{ + /// + /// Single entry point of the UI: connects the views to the player and switches screens. + /// + public class UiManager : MonoBehaviour + { + [System.Serializable] + public struct References + { + [Tooltip("In-game screen: health, ammo.")] + public GameObject Hud; + public HealthView HealthView; + public AmmoView AmmoView; + public GameOverView GameOverView; + } + + #region Fields + + [SerializeField] private References _references; + + private Player _player; + + #endregion + + #region Unity Lifecycle + + private void Start() + { + // Start: every Awake has run, so Player.Instance is set + _player = Player.Instance; + + if (_player == null) + { + Debug.LogError("UiManager: no Player in the scene."); + return; + } + + _references.HealthView.Bind(_player.Health); + _references.AmmoView.Bind(_player.Launcher); + _player.Health.OnDied += HandlePlayerDied; + + _references.Hud.SetActive(true); + _references.GameOverView.Hide(); + } + + private void OnDestroy() + { + if (_player != null) + _player.Health.OnDied -= HandlePlayerDied; + } + + #endregion + + #region Private Methods + + private void HandlePlayerDied() + { + _references.Hud.SetActive(false); + _references.GameOverView.Show(); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/Combat/UiManager.cs.meta b/Assets/_Content/Scripts/Combat/UiManager.cs.meta new file mode 100644 index 0000000..a293b8c --- /dev/null +++ b/Assets/_Content/Scripts/Combat/UiManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4c6cb5ac8e4f8d0479afda241378ff0b \ No newline at end of file diff --git a/Assets/_Content/Scripts/Combat/WeaponData.cs b/Assets/_Content/Scripts/Combat/WeaponData.cs new file mode 100644 index 0000000..6bf887e --- /dev/null +++ b/Assets/_Content/Scripts/Combat/WeaponData.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +namespace Arena.Combat +{ + /// + /// Settings of a weapon, shared by every launcher that uses it. + /// + [CreateAssetMenu(fileName = "WeaponData_new", menuName = "Arena/Combat/Weapon Data")] + public class WeaponData : ScriptableObject + { + #region Fields + + [Tooltip("Damage dealt by one projectile.")] + [SerializeField, Min(0)] private int _damage = 10; + + [Tooltip("Time between two shots, in seconds.")] + [SerializeField, Min(0.05f)] private float _fireInterval = 0.2f; + + [Tooltip("Ammo when the weapon is full.")] + [SerializeField, Min(1)] private int _maxAmmo = 30; + + [Tooltip("Prefab fired by the weapon.")] + [SerializeField] private Projectile _projectilePrefab; + + public int Damage => _damage; + public float FireInterval => _fireInterval; + public int MaxAmmo => _maxAmmo; + public Projectile ProjectilePrefab => _projectilePrefab; + + #endregion + } +} diff --git a/Assets/_Content/Scripts/Combat/WeaponData.cs.meta b/Assets/_Content/Scripts/Combat/WeaponData.cs.meta new file mode 100644 index 0000000..63ac005 --- /dev/null +++ b/Assets/_Content/Scripts/Combat/WeaponData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b4a4852c6f394a441935fae7b9f9ec82 \ No newline at end of file diff --git a/Assets/_Content/Scripts/PlayerMover.cs b/Assets/_Content/Scripts/PlayerMover.cs deleted file mode 100644 index 0a4cd08..0000000 --- a/Assets/_Content/Scripts/PlayerMover.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.ComponentModel; -using UnityEngine; - -public class PlayerMover : MonoBehaviour -{ - [System.Serializable] - private class Settings - { - public float Speed = 5; - } - - [System.Serializable] - private struct References - { - public CharacterController Controller; - } - - [System.Serializable] - public class StateContainer - { - public bool IsJumping = false; - } - -#region FIELDS - - const float MS_TO_KMH = 1 / 3.6f; - - [SerializeField] private Settings _settings; - [SerializeField] private References _references; - [SerializeField] private StateContainer _state; - - public StateContainer State => _state; - -#endregion - -#region UNITY LIFECYCLES - - void Update() - { - float t = Time.deltaTime; - - Move(t); - } - - private void Move(float t) - { - transform.position += Vector3.forward * t * MS_TO_KMH * _settings.Speed; - } - -#endregion - - -} \ No newline at end of file diff --git a/Assets/_Content/Scripts/UI.meta b/Assets/_Content/Scripts/UI.meta new file mode 100644 index 0000000..d8089f5 --- /dev/null +++ b/Assets/_Content/Scripts/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e11b6fbe420e7b94b8000b7e9c608900 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Content/Scripts/UI/AmmoView.cs b/Assets/_Content/Scripts/UI/AmmoView.cs new file mode 100644 index 0000000..0b3760a --- /dev/null +++ b/Assets/_Content/Scripts/UI/AmmoView.cs @@ -0,0 +1,59 @@ +using TMPro; +using UnityEngine; +using Arena.Combat; + +namespace Arena.UI +{ + /// + /// Displays the ammo of a launcher as "current / max". + /// + public class AmmoView : MonoBehaviour + { + [System.Serializable] + public struct References + { + public TMP_Text Label; + } + + #region Fields + + [SerializeField] private References _references; + + private ProjectileLauncher _launcher; + + #endregion + + #region Unity Lifecycle + + private void OnDestroy() + { + if (_launcher != null) + _launcher.OnAmmoChange -= HandleAmmoChange; + } + + #endregion + + #region Public Methods + + /// + /// Starts displaying the ammo of a launcher. Called by the UiManager. + /// + public void Bind(ProjectileLauncher launcher) + { + _launcher = launcher; + _launcher.OnAmmoChange += HandleAmmoChange; + HandleAmmoChange(_launcher.State.Ammo, _launcher.MaxAmmo); + } + + #endregion + + #region Private Methods + + private void HandleAmmoChange(int current, int max) + { + _references.Label.SetText("{0} / {1}", current, max); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/UI/AmmoView.cs.meta b/Assets/_Content/Scripts/UI/AmmoView.cs.meta new file mode 100644 index 0000000..2636c79 --- /dev/null +++ b/Assets/_Content/Scripts/UI/AmmoView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ca9ade34ced9c264fad512c9408d61d8 \ No newline at end of file diff --git a/Assets/_Content/Scripts/UI/GameOverView.cs b/Assets/_Content/Scripts/UI/GameOverView.cs new file mode 100644 index 0000000..d58a46a --- /dev/null +++ b/Assets/_Content/Scripts/UI/GameOverView.cs @@ -0,0 +1,33 @@ +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Arena.UI +{ + /// + /// Game over screen. Its Restart button reloads the scene. + /// + public class GameOverView : MonoBehaviour + { + #region Public Methods + + public void Show() + { + gameObject.SetActive(true); + } + + public void Hide() + { + gameObject.SetActive(false); + } + + /// + /// Called by the Restart button (OnClick in the Inspector). + /// + public void Restart() + { + SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/UI/GameOverView.cs.meta b/Assets/_Content/Scripts/UI/GameOverView.cs.meta new file mode 100644 index 0000000..2fb52c2 --- /dev/null +++ b/Assets/_Content/Scripts/UI/GameOverView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 78d77256975d6d7499a20292c00b0649 \ No newline at end of file diff --git a/Assets/_Content/Scripts/UI/HealthView.cs b/Assets/_Content/Scripts/UI/HealthView.cs new file mode 100644 index 0000000..d928c43 --- /dev/null +++ b/Assets/_Content/Scripts/UI/HealthView.cs @@ -0,0 +1,60 @@ +using TMPro; +using UnityEngine; +using Arena.Combat; + +namespace Arena.UI +{ + /// + /// Displays a Health as "current / max". + /// + public class HealthView : MonoBehaviour + { + [System.Serializable] + public struct References + { + public TMP_Text Label; + } + + #region Fields + + [SerializeField] private References _references; + + private Health _health; + + #endregion + + #region Unity Lifecycle + + private void OnDestroy() + { + // Every subscription has its unsubscription + if (_health != null) + _health.OnChange -= HandleHealthChange; + } + + #endregion + + #region Public Methods + + /// + /// Starts displaying a Health. Called by the UiManager. + /// + public void Bind(Health health) + { + _health = health; + _health.OnChange += HandleHealthChange; + HandleHealthChange(_health.Current, _health.Max); + } + + #endregion + + #region Private Methods + + private void HandleHealthChange(int current, int max) + { + _references.Label.SetText("{0} / {1}", current, max); + } + + #endregion + } +} diff --git a/Assets/_Content/Scripts/UI/HealthView.cs.meta b/Assets/_Content/Scripts/UI/HealthView.cs.meta new file mode 100644 index 0000000..8307f34 --- /dev/null +++ b/Assets/_Content/Scripts/UI/HealthView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c96e24dd38a22b546a15f929ee0f0c91 \ No newline at end of file