Implement IDamageable interface and Health management for player and enemy controllers

This commit is contained in:
Данил Ярош 2025-10-08 11:02:42 +03:00
parent d821e7c1df
commit a6bb1f9114
47 changed files with 160 additions and 39 deletions

View File

@ -24,6 +24,13 @@ namespace RR.Common.Interfaces
public bool TryPickup(GameObject newOwner, Transform optionaAttachPoint = null);
}
public interface IDamageable
{
void TakeDamage(int amount);
void Heal(int amount);
void Die();
}
}
namespace RR.Game2D.Core.Interfaces

View File

@ -1,13 +1,15 @@
using UnityEngine;
using RR.Game2D.Core;
using RR.Common.Interfaces;
namespace RR.Game2D.Behaviour
{
[RequireComponent(typeof(Rigidbody2D),
typeof(Collider2D),
typeof(Animator))]
public class EnemyController : MonoBehaviour
public class EnemyController : MonoBehaviour, IDamageable
{
[Header("Обзор")]
[SerializeField] private float viewDistance = 5f;
[SerializeField] private float viewAngle = 60f;
@ -19,12 +21,21 @@ namespace RR.Game2D.Behaviour
[Header("Препятствия")]
[SerializeField] private LayerMask obstacleMask;
[Header("Здоровье")]
[SerializeField] private int maxHealth = 1;
private Health health;
public int CurrentHealth => health?.CurrentHealth ?? 0;
public int MaxHealth => health?.MaxHealth ?? 0;
private Animator animator;
private Rigidbody2D rb;
private Collider2D col;
private GameObject playerObj;
// Аниматор
private static readonly int Speed = Animator.StringToHash("Speed");
void Start()
{
rb = GetComponent<Rigidbody2D>();
@ -36,13 +47,22 @@ namespace RR.Game2D.Behaviour
{
playerObj = GameObject.FindGameObjectWithTag("Player");
}
health = GetComponent<Health>();
if (health == null)
{
health = gameObject.AddComponent<Health>();
}
health.ResetHealth();
health.SetOwner(this);
health.SetMaxHealth(maxHealth);
}
void FixedUpdate()
{
if (!playerObj) return;
Vector2 playerCenter = (Vector2)playerObj.transform.position;
Vector2 playerCenter = playerObj.transform.position;
if (PlayerInSight())
{
@ -60,7 +80,7 @@ namespace RR.Game2D.Behaviour
rb.linearVelocity, Vector2.zero, acceleration * Time.fixedDeltaTime);
}
animator.SetFloat("Speed", Mathf.Abs(rb.linearVelocity.magnitude));
animator.SetFloat(Speed, Mathf.Abs(rb.linearVelocity.magnitude));
}
void OnCollisionEnter2D(Collision2D collision)
@ -70,20 +90,21 @@ namespace RR.Game2D.Behaviour
Rigidbody2D playerRb = collision.collider.GetComponent<Rigidbody2D>();
playerObj.TryGetComponent(out PlayerController playerController);
if (playerRb == null) return;
Vector2 contactNormal = collision.GetContact(0).normal;
if (contactNormal.y < -0.5f)
{
playerRb.linearVelocity = new Vector2(playerRb.linearVelocity.x, 35f);
Die();
TakeDamage(MaxHealth);
}
else
{
playerController.Die();
playerController.TakeDamage(1);
}
}
else if (collision.collider.CompareTag("Projectile")) Die();
else if (collision.collider.CompareTag("Projectile"))
{
TakeDamage(1);
}
}
private bool PlayerInSight()
@ -116,9 +137,16 @@ namespace RR.Game2D.Behaviour
return true;
}
public void TakeDamage(int amount)
{
health.TakeDamage(amount);
}
public void Heal(int amount)
{
health.Heal(amount);
}
public void Die()
{
//animator.Play("Death", 0); // TODO: Анимация смерти, потом смерть
Destroy(gameObject);
}
@ -131,8 +159,8 @@ namespace RR.Game2D.Behaviour
Vector2 forward = transform.localEulerAngles.y >= 180f ? Vector2.right : Vector2.left;
float half = viewAngle * 0.5f;
Vector2 edgeA = (Vector2)(Quaternion.Euler(0, 0, +half) * (Vector3)forward);
Vector2 edgeB = (Vector2)(Quaternion.Euler(0, 0, -half) * (Vector3)forward);
Vector2 edgeA = (Quaternion.Euler(0, 0, +half) * forward);
Vector2 edgeB = (Quaternion.Euler(0, 0, -half) * forward);
Gizmos.color = Color.yellow;
Gizmos.DrawLine(origin, origin + edgeA * viewDistance);

46
Assets/Scripts/Health.cs Normal file
View File

@ -0,0 +1,46 @@
using UnityEngine;
using RR.Common.Interfaces;
namespace RR.Game2D.Behaviour
{
[System.Serializable]
public class Health : MonoBehaviour
{
[SerializeField] private int maxHealth = 3;
[SerializeField] private int currentHealth;
private IDamageable owner;
public int MaxHealth => maxHealth;
public int CurrentHealth => currentHealth;
public void SetOwner(IDamageable owner)
{
this.owner = owner;
}
public void SetMaxHealth(int value)
{
maxHealth = value;
currentHealth = maxHealth;
}
public void TakeDamage(int amount)
{
currentHealth -= amount;
if (currentHealth <= 0)
{
owner?.Die();
}
}
public void Heal(int amount)
{
currentHealth = Mathf.Min(currentHealth + amount, maxHealth);
}
public void ResetHealth()
{
currentHealth = maxHealth;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1a9fe08f70754d1fa12c13ac4cd190e3
timeCreated: 1759909224

View File

@ -0,0 +1,3 @@
namespace RR.Game2D.Core
{
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ebc3129eb7104067b88015c32e09cdf8
timeCreated: 1759909224

View File

@ -1,14 +1,15 @@
using RR.Game2D.Core;
using UnityEngine;
using UnityEngine;
using UnityEngine.InputSystem;
using RR.Common.Interfaces;
using RR.Game2D.Core;
namespace RR.Game2D.Behaviour
{
[DisallowMultipleComponent]
[RequireComponent(typeof(Rigidbody2D),
typeof(Collider2D),
typeof(Animator))]
public class PlayerController : MonoBehaviour
typeof(Collider2D),
typeof(Animator))]
public class PlayerController : MonoBehaviour, IDamageable
{
[Header("Управление")]
@ -32,9 +33,15 @@ namespace RR.Game2D.Behaviour
[Header("Аниматор [Опционально]")]
[SerializeField] private Animator animator; // оставить в инспекторе на всякий случай
[Header("Здоровье")]
[SerializeField] private int maxHealth = 3;
private Health health;
public int CurrentHealth => health?.CurrentHealth ?? 0;
public int MaxHealth => health?.MaxHealth ?? 0;
private Rigidbody2D rb;
//private Collider2D col;
private float inputX;
private bool jumpPressed, jumpReleased, jumpHold;
@ -73,9 +80,17 @@ namespace RR.Game2D.Behaviour
animator = GetComponent<Animator>();
if (animator == null)
animator = GetComponentInChildren<Animator>(true);
if (animator == null)
Debug.LogError($"[{name}] Animator не найден!");
health = GetComponent<Health>();
if (health == null)
{
health = gameObject.AddComponent<Health>();
}
health.ResetHealth();
health.SetOwner(this);
health.SetMaxHealth(maxHealth);
}
void Update()
@ -145,9 +160,26 @@ namespace RR.Game2D.Behaviour
{
GameManager.Instance.NextLevel();
}
if (collision.CompareTag("Enemy"))
{
TakeDamage(1);
}
}
//private void OnDestroy() => Debug.Log("Player destroyed");
public void TakeDamage(int amount)
{
health.TakeDamage(amount);
}
public void Heal(int amount)
{
health.Heal(amount);
}
public void Die()
{
GameManager.Instance.ResetLevel();
}
/// <summary>
/// Метод для проверки соприкосновения с землей
@ -164,14 +196,5 @@ namespace RR.Game2D.Behaviour
return isGrounded;
}
/// <summary>
/// Метод смерти персонажа.
/// Базовое поведение - сброс уровня в GameManager
/// </summary>
public void Die()
{
GameManager.Instance.ResetLevel();
}
}
}

View File

@ -1,16 +1,24 @@
using UnityEngine;
public class ReadOnlyInInspectorAttribute : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
#if UNITY_EDITOR
// Update is called once per frame
void Update()
using UnityEditor;
#endif
public class ReadOnlyInInspectorAttribute : PropertyAttribute {}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ReadOnlyInInspectorAttribute))]
public class ReadOnlyDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> EditorGUI.GetPropertyHeight(property, label, true);
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
GUI.enabled = false; EditorGUI.PropertyField(position, property, label, true); GUI.enabled = true;
}
}
#endif