Máu, sát thương và cảm giác trúng đòn
Assets/
Scenes/
Arena.unity
Scripts/
thiết lập dự án
đã 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.
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
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
Tạo layer tên Enemy: , rồi gán layer đó cho Dummy.
- 3
Gắn CharacterStats vào nó và đặt maxHealth là 120.
- 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
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.
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
✖ Update() polling
Dò trong Update
Bước 3 — thanh máu trên đầu
- 1
Trên Dummy, thêm con rồi đặt Render Mode = World Space.
- 2
Đặt RectTransform của Canvas: width 120, height 20, scale 0.01, position Y = 2.4.
- 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
Bên trong đặt hai Image: một nền tối, và một Fill màu đỏ với Image Type = Filled, Horizontal.
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
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
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 đã.