U
tab 02Nhân vật & Chiến đấu9 bài — chạm để mở
07 / 09 · 11 phút
Nhân vật & Chiến đấu

Máu, sát thương và cảm giác trúng đòn

dự án của bạn tới lúc này

Assets/

Scenes/

Arena.unity

KnightDummy ×3

Scripts/

CharacterStats.csSkillCaster.csEquipment.csCharacterStats.OnDied eventHealthBar.csDamagePopup.csHitFlash.cs

thiết lập dự án

Enemy layer

đã có sẵnbài này thêm

Bạn đã có skill gây sát thương, nhưng chưa có gì để đánh và cũng không thấy được là nó có ăn hay không. Cú đánh mà người chơi không cảm nhận được thì coi như không tồn tại.

sơ đồ động

Ba tín hiệu làm nên cảm giác trúng đòn: con số nhảy ra, thân thể chớp trắng, thanh máu tụt xuống.

Bước 1 — có cái để đánh

  1. 1

    Tạo một Capsule, đặt tên Dummy, gán material đỏ và đặt cách Knight 6 m. Đúng vậy — nó chính là loại capsule mà Knight từng là hồi đầu tab này.

  2. 2

    Tạo layer tên Enemy: Inspector▸Layer▸Add Layer, rồi gán layer đó cho Dummy.

  3. 3

    Gắn CharacterStats vào nó và đặt maxHealth là 120.

  4. 4

    Đặt mask Enemy Layers trên SkillCaster của Knight chỉ chọn Enemy — giờ Q, E, R trúng bù nhìn chứ không bao giờ trúng Mage hay Archer.

  5. 5

    Nhân bản bù nhìn thêm hai lần và rải ba con quanh nhân vật.

Bước 2 — dùng sự kiện, đừng dò liên tục

Thanh máu không nên kiểm tra máu mỗi khung hình. Thay vào đó, CharacterStats thông báo mỗi khi máu đổi, ai quan tâm thì lắng nghe.

add to CharacterStats.cs
using System;
using UnityEngine;

//bên trong CharacterStats

//máu hiện tại và máu tối đa
public event Action<float, float> OnHealthChanged;
public event Action OnDied;

public void TakeDamage(float rawDamage)
{
    if (!IsAlive) return;

    float reduced = Mathf.Max(1f, rawDamage - finalDefense);
    currentHealth = Mathf.Max(0f, currentHealth - reduced);

    OnHealthChanged?.Invoke(currentHealth, finalMaxHp);   // báo cho ai đang nghe
    DamagePopup.Spawn(transform.position + Vector3.up * 2f, reduced);

    if (!IsAlive)
    {
        OnDied?.Invoke();
        Die();
    }
}

public void Heal(float amount)
{
    if (!IsAlive) return;
    currentHealth = Mathf.Min(finalMaxHp, currentHealth + amount);
    OnHealthChanged?.Invoke(currentHealth, finalMaxHp);
}

✓ Event

Sự kiện — chạy khi có thay đổi

Chỉ chạy đúng khung hình có sát thương. Những lúc khác không tốn gì.

✖ Update() polling

Dò trong Update

Đọc giá trị 60 lần/giây cho 30 con địch chỉ để bắt hai khung hình có ý nghĩa.

Bước 3 — thanh máu trên đầu

  1. 1

    Trên Dummy, thêm con UI▸Canvas rồi đặt Render Mode = World Space.

  2. 2

    Đặt RectTransform của Canvas: width 120, height 20, scale 0.01, position Y = 2.4.

  3. 3

    Scale 0.01 biến 120 pixel UI thành 1,2 mét — không có nó thì thanh máu to bằng toà nhà.

  4. 4

    Bên trong đặt hai Image: một nền tối, và một Fill màu đỏ với Image Type = Filled, Horizontal.

HealthBar.cs — put it on the world-space Canvas
using UnityEngine;
using UnityEngine.UI;

public class HealthBar : MonoBehaviour
{
    [SerializeField] private CharacterStats stats;
    [SerializeField] private Image fill;
    [SerializeField] private Image trailing;      // thanh vàng chạy sau
    [SerializeField] private float trailSpeed = 1.2f;

    private Camera cam;
    private float target = 1f;

    void Awake() => cam = Camera.main;

    void OnEnable()
    {
        stats.OnHealthChanged += Redraw;
        stats.OnDied += () => gameObject.SetActive(false);
    }

    void OnDisable() => stats.OnHealthChanged -= Redraw;   // luôn nhớ huỷ đăng ký

    void Redraw(float current, float max)
    {
        target = max <= 0f ? 0f : current / max;
        fill.fillAmount = target;                          // tụt ngay
    }

    void LateUpdate()
    {
        // thanh chạy sau tụt từ từ — chính nó làm người chơi thấy rõ mất bao nhiêu máu
        trailing.fillAmount = Mathf.MoveTowards(trailing.fillAmount, target, trailSpeed * Time.deltaTime);

        //luôn quay mặt về camera
        transform.rotation = cam.transform.rotation;
    }
}

Bước 4 — số sát thương bay lên

DamagePopup.cs — on a small world-space Canvas prefab
using UnityEngine;
using TMPro;

public class DamagePopup : MonoBehaviour
{
    private static DamagePopup prefab;

    [SerializeField] private TextMeshProUGUI label;
    [SerializeField] private float lifeTime = 0.8f;
    [SerializeField] private float riseSpeed = 1.6f;

    private float age;

    public static void Spawn(Vector3 worldPos, float amount)
    {
        if (prefab == null)
            prefab = Resources.Load<DamagePopup>("DamagePopup");   // Assets/Resources/DamagePopup.prefab
        if (prefab == null) return;

        DamagePopup p = Instantiate(prefab, worldPos, Quaternion.identity);
        p.label.text = Mathf.RoundToInt(amount).ToString();

        //lệch nhẹ để hai cú đánh không chồng nhau
        p.transform.position += new Vector3(Random.Range(-0.3f, 0.3f), 0f, 0f);
    }

    void Update()
    {
        age += Time.deltaTime;

        transform.position += Vector3.up * riseSpeed * Time.deltaTime;
        transform.rotation = Camera.main.transform.rotation;

        float t = age / lifeTime;
        label.alpha = 1f - t;                                     // mờ dần
        transform.localScale = Vector3.one * (1f + 0.3f * (1f - t));

        if (age >= lifeTime) Destroy(gameObject);
    }
}

Cú chớp trắng

HitFlash.cs — on the dummy
using System.Collections;
using UnityEngine;

public class HitFlash : MonoBehaviour
{
    [SerializeField] private CharacterStats stats;
    [SerializeField] private Renderer[] renderers;
    [SerializeField] private float duration = 0.09f;

    private Color[] original;

    void Awake()
    {
        original = new Color[renderers.Length];
        for (int i = 0; i < renderers.Length; i++)
            original[i] = renderers[i].material.color;
    }

    void OnEnable()  => stats.OnHealthChanged += (_, __) => { StopAllCoroutines(); StartCoroutine(Flash()); };

    IEnumerator Flash()
    {
        foreach (Renderer r in renderers) r.material.color = Color.white;
        yield return new WaitForSeconds(duration);
        for (int i = 0; i < renderers.Length; i++) renderers[i].material.color = original[i];
    }
}
  • 0,09 giây là vừa đẹp. Ngắn hơn thì không ai thấy; dài hơn thì con địch nhấp nháy như đèn disco.

  • Đọc .material tạo ra một bản sao material riêng cho object đó. Nhờ vậy một con chớp trắng mà không kéo theo cả đàn.

  • Ghép cú chớp với khựng hình 0,05 giây ở bài VFX là đấm một cái capsule cũng thấy đã.