U
章 02角色与战斗9 课 — 点击打开
05 / 09 · 11 分钟
角色与战斗

弹道、伤害与特效

到目前为止的项目

Assets/

Scripts/

SkillCaster.csProjectile.csSimplePool.cs

Prefabs/

P_Fireball

Data/

Skill_Slash / Dash / Ultimate

你已经有的这一课新增

弹体就是一个向前飞、撞上东西、造成伤害然后消失的 prefab。四个职责,一个小脚本。

动态图解

Knight 沿着斜向的一排格子开火,靶子挨了一下,弹体又回到它出来的那个 pool 里。

弹体

Projectile.cs
using UnityEngine;

[RequireComponent(typeof(Collider))]
public class Projectile : MonoBehaviour
{
    [SerializeField] private float speed = 18f;
    [SerializeField] private float lifeTime = 4f;
    [SerializeField] private GameObject hitVfx;

    private float damage;
    private GameObject owner;                 // do not hit the shooter

    public void Launch(GameObject shooter, float dmg)
    {
        owner = shooter;
        damage = dmg;
        Destroy(gameObject, lifeTime);        // clean up if it never hits
    }

    void Update()
    {
        transform.position += transform.forward * speed * Time.deltaTime;
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.gameObject == owner) return;

        if (other.TryGetComponent(out CharacterStats target))
            target.TakeDamage(damage);

        if (hitVfx != null)
            Instantiate(hitVfx, transform.position, Quaternion.identity);

        Destroy(gameObject);
    }
}
  1. 1

    GameObject▸3D Object▸Sphere,scale 设为 0.3,命名为 P_Fireball。

  2. 2

    在它的 Collider 上勾选 Is Trigger —— trigger 只报告重叠,不会发生物理反弹。

  3. 3

    挂上 Projectile.cs,给它一个自发光的 material,再拖进 Assets/Prefabs。

  4. 4

    把它从场景里删掉。prefab 应该待在 Project 里,而不是 Hierarchy。

从技能里发射

add to SkillData and SkillCaster
// in SkillData.cs
public GameObject projectilePrefab;

// in SkillCaster.TryCast
if (s.projectilePrefab != null)
{
    GameObject go = Instantiate(s.projectilePrefab, castPoint.position, castPoint.rotation);
    go.GetComponent<Projectile>().Launch(gameObject, s.damage);
}

对象池 —— 告别连射时的卡顿

Instantiate 和 Destroy 都要分配内存。每秒打 20 发,垃圾回收就会带来卡顿。用池就能一直重复利用同一批对象。

SimplePool.cs
using System.Collections.Generic;
using UnityEngine;

public class SimplePool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int prewarm = 20;

    private readonly Queue<GameObject> pool = new Queue<GameObject>();

    void Awake()
    {
        for (int i = 0; i < prewarm; i++)
        {
            GameObject go = Instantiate(prefab, transform);
            go.SetActive(false);
            pool.Enqueue(go);
        }
    }

    public GameObject Get(Vector3 pos, Quaternion rot)
    {
        GameObject go = pool.Count > 0 ? pool.Dequeue() : Instantiate(prefab, transform);
        go.transform.SetPositionAndRotation(pos, rot);
        go.SetActive(true);
        return go;
    }

    public void Release(GameObject go)
    {
        go.SetActive(false);
        pool.Enqueue(go);
    }
}
  • 把弹体里的 Destroy(gameObject) 换成回调 pool.Release(gameObject)。

  • 如果喜欢用现成的实现,Unity 也自带 UnityEngine.Pool.ObjectPool<T>。

  • 凡是每秒要生成好几次的一切都做成池:子弹、命中火花、伤害数字。

看得清楚的特效

  • 在 Particle System 上把 Stop Action 设为 Destroy,一次性特效就会自己清理干净。

  • 命中火花持续 0.2–0.4 秒。再长下去屏幕就糊成一锅粥了。

  • 一点点镜头震动(0.1 秒)加上 0.05 秒顿帧,比任何粒子效果都更能卖出打击感。

hit-stop
using System.Collections;

IEnumerator HitStop(float seconds)
{
    Time.timeScale = 0.05f;
    yield return new WaitForSecondsRealtime(seconds);   // Realtime! timeScale is frozen
    Time.timeScale = 1f;
}

// call it
StartCoroutine(HitStop(0.05f));

常见错误

  • 弹体先打中开枪的人:它在 Knight 自己的碰撞体内部生成,OnTriggerEnter 就在施法者身上触发了。改从身体前方一点的 castPoint 生成,并忽略 transform.root 是施法者的命中。

  • 从池里取回的弹体带着上一次的状态:要在 OnEnable 里重置位置、旋转和所有计时器,否则第二发要么立刻消失,要么飞错方向。

  • 换成对象池之后 Destroy(gameObject, lifeTime) 还留着,打空的弹体就会把池里的对象销毁掉,池子越来越空。两处 Destroy 都换成 pool.Release,定时的那一处用 Invoke 或协程来调。

  • 飞得快的弹体会穿过薄碰撞体:30 m/s 配 30 FPS,子弹一帧就走整整一米,可能直接跳过一堵墙。通过 Rigidbody 的 MovePosition 移动并把 Collision Detection 设为 Continuous,或者把速度控制在合理范围。