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