Unity2D游戏开发——Sprite碎裂特效的实现(独立解耦的组件,附详细流程和代码)

先上实际效果

效果
如上图所示,效果很直观,即原始的Sprite图像的破碎组件触发后,会将原图碎裂成无数小块,且使之炸裂。那么,要实现这个功能就有如下几点需求:

  • 对于任意大小形状的Sprite,仅通过一个方法就能使其破碎;
  • 尽可能的保证碎片的形状大小具有随机性,但是碎片不能太大,否则不美观;
  • 触发完之后要让碎片炸开来;
  • 考虑到复用性,要封装成一个组件,以便与工程解耦。

一、Sprite随机分割点生成

我们依然拿原图举例子。随机裁切的本质,即是在Sprite的矩形框内,随机找分割点,并对分割的轴做垂线,重新生成四个新的矩形区域,如下图所示:
分割点
只不过,用完全随机的方式来生成多个切割点的时候,如果两个点的坐标比较相近,那么实际效果就会很不美观(有些地方特别大,有些地方特别小),如下图:
在这里插入图片描述
所以,我们的目的是要分割点均匀的随机(没错,就是有前提的随机);
举个例子:如果我们希望生成9张碎裂后的子图片,则需要有2个分割点来分割图像,即N个分割点,生成(N+1)(N+1)张子图,可以采取如下图的区域划分方法。

  1. 先将区域划分为5X5的区域(即(2N+1)*(2N+1));
  2. 设左上方的子区域为S00,则于主对角线上,所有i和j坐标均为奇数的区域Sij内生成随机数(黄点所在区域);
  3. 对随机数生成的坐标进行升序排序;
  4. 从左上角的点依次分割至左下角。
    割点生成位置
    算法如上所述那样,通过区域划分法,在对应子区域内生成分割点,即可以保证随机性,又不会使分割效果太丑。那么,生成了随机分割点之后,我们应该如果去分割Sprite呢?

二、Sprite的裁切

U3D的Sprite类内并没有直接提供裁切的API,但是Sprite.Create()方法却可以以Sprite对象的一部分来生成一个新的Sprite,它的API是这样的:

Sprite.Create(Texture texture, Rect rect, Vector2 vector);

第二个参数就是新Sprite对应原始Sprite的矩形区域。

三、碎片弹射

碎片弹射效果,我们采用的是给碎片加力的方式来实现的。这样一来,就要求碎片必须是一个刚体,于是,在碎片对象生成的部分,我们使用如下代码来执行。

	/// <summary>
    /// 弹射一个碎片对象。
    /// </summary>
    /// <param name="fragment">碎片对象。</param>
    private void Ejection(GameObject fragment)
    {
        Vector2 start = fragment.transform.position;
        Vector2 end = gameObject.transform.position;
        Vector2 direction = end - start;
        fragment.GetComponent<Rigidbody2D>().AddForce(direction * forceMultiply, ForceMode2D.Impulse);
    }

	/// <summary>
    /// 创造一个碎片对象。
    /// </summary>
    /// <param name="sprite">碎片贴图。</param>
    /// <param name="position">碎片贴图位置。</param>
    /// <returns>碎片对象。</returns>
    private GameObject CreateFragment(Sprite sprite, Vector2 position)
    {
        GameObject fragment = new GameObject("Fragment");
        fragment.layer = LayerMask.NameToLayer(layerName);
        fragment.transform.position = position;
        fragment.AddComponent<SpriteRenderer>().sprite = sprite;
        // 可以将碎片视作刚体,这样会有与地形的碰撞效果
        fragment.AddComponent<Rigidbody2D>();
        fragment.AddComponent<BoxCollider2D>();
        fragment.AddComponent<FadeOut>().delaySecond = delaySecond;     // 添加淡出效果
        return fragment;
    }

在上述代码中,我们默认了碎片是刚体,我们还添加了一个碰撞盒,以便它在与地形碰撞时有真实的物理效果,而不是穿模。但是,能碰撞的刚体碎片存在一个问题,就是它也会与玩家/敌人对象碰撞,可能会因为XX碎了一地而把角色卡在墙角,所以,我们传入一个LayerName参数,去标定碎片生成的Layer,并在Project Setting–>Physics2D中修改碰撞列表,使它只与特地Layer的物体碰撞。这种方式会让Unity直接过滤两个Layer之间的碰撞(不会被OnTrigger/OnCollision检测到),效率较高。
修改碰撞列表

四、碎片回收

碎片本质上是一种垃圾对象,它在效果执行完毕之后便应当被立即回收掉,我们可以单独构造一个FadeOut组件类,让它持续一段时间后能够自动消失。 (注:此组件会逐渐使物体的alpha值减小,减为0时Destroy掉物体。如果不需要改变alpha值而直接Destroy的话,就使用协程来做) FadeOut类的代码如下:

using System.Collections;
using UnityEngine;

/// <summary>
/// 淡出效果组件类。
/// </summary>
public class FadeOut : MonoBehaviour
{
    #region 可视变量
    [HideInInspector] [Tooltip("消失时延。")] public float delaySecond = 5F;
    #endregion

    #region 成员变量
    private SpriteRenderer spriteRenderer = null;
    private float fadeSpeed = 0;    // 消逝速度
    #endregion

    #region 功能方法
    /// <summary>
    /// 第一帧调用之前触发。
    /// </summary>
    private void Start()
    {
        if (TryGetComponent(out SpriteRenderer spriteRenderer))
            this.spriteRenderer = spriteRenderer;
        fadeSpeed = this.spriteRenderer.color.a * Time.fixedDeltaTime / delaySecond;
        //StartCoroutine(DestroyNow());
    }

    /*
    /// <summary>
    /// 定时自杀。
    /// </summary>
    /// <returns></returns>
    private IEnumerator DestroyNow()
    {
        yield return new WaitForSeconds(delaySecond);
        Destroy(gameObject);
    }
    */

    /// <summary>
    /// 降低对象透明度,为0后摧毁对象。
    /// 在固定物理帧刷新时触发。
    /// </summary>
    private void FixedUpdate()
    {
        float alpha = spriteRenderer.color.a - fadeSpeed;
        spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.r, spriteRenderer.color.r, alpha);
        if (alpha <= 0)
            Destroy(gameObject);
    }
    #endregion
}

完整代码

  1. 排序算法Sort类:
using System;

/// <summary>
/// 排序算法类。
/// </summary>
public class Sort<T> where T : IComparable
{
    #region 基础公有方法
    /// <summary>
    /// 数组快速排序。
    /// </summary>
    /// <param name="array">待排序数组。</param>
    /// <param name="low">排序起点。</param>
    /// <param name="high">排序终点。</param>
    public void QuickSort(T[] array, int low, int high)
    {
        if (low >= high)
            return;
        int first = low;
        int last = high;
        T key = array[low];
        while (first < last)
        {
            while (first < last && CompareGeneric(array[last], key) >= 0)
                last--;
            array[first] = array[last];
            while (first < last && CompareGeneric(array[first], key) <= 0)
                first++;
            array[last] = array[first];
        }
        array[first] = key;
        QuickSort(array, low, first - 1);
        QuickSort(array, first + 1, high);
    }
    #endregion

    #region 静态私有方法
    /// <summary>
    /// 泛型对象比较大小。
    /// </summary>
    /// <param name="t1">待比较对象。</param>
    /// <param name="t2">待比较对象。</param>
    /// <returns>大于0则前者的值更大,小于0则反之,等于0则二者的值相等。</returns>
    private static int CompareGeneric(T t1, T t2)
    {
        if (t1.CompareTo(t2) > 0)
            return 1;
        else if (t1.CompareTo(t2) == 0)
            return 0;
        else
            return -1;
    }
    #endregion
}
  1. 核心代码Crasher组件类:
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 物体分裂效果组件类。
/// </summary>
public class Crasher : MonoBehaviour
{
    #region 可视变量
    [SerializeField]
    [Tooltip("Sprite对象。")]
    private Sprite sprite = null;

    [SerializeField]
    [Tooltip("碎片的层次名称,用于避碰。")]
    private string layerName = "Fragment";

    [SerializeField] [Tooltip("分割点的数量。")]
    private int splitPoint = 3;

    [SerializeField] [Tooltip("爆破力乘数。")]
    private float forceMultiply = 50F;

    [SerializeField] [Tooltip("碎片消失时延。")]
    private float delaySecond = 5F;
    #endregion

    #region 成员变量
    private int seed = 0;               // 随机数种子
    private float spriteWidth = 0;      // 贴图实际宽度
    private float spriteHeight = 0;     // 贴图实际高度
    private List<GameObject> fragments = new List<GameObject>();    // 碎片对象列表
    #endregion

    #region 功能方法
    /// <summary>
    /// 对对象执行粉碎特效。
    /// </summary>
    public void Crash()
    {
        // 属性初始化
        spriteWidth = sprite.texture.width;
        spriteHeight = sprite.texture.height;
        // 获取所有碎片对象
        GetFragments(sprite.texture, RandomSplits());
        // 弹射碎片对象
        for (int i = 0; i < fragments.Count; i++)
            Ejection(fragments[i]);
    }

    /// <summary>
    /// 根据割点获取所有碎片对象。
    /// </summary>
    /// <param name="texture2D">原始对象的纹理。</param>
    /// <param name="splits">割点列表。</param>
    private void GetFragments(Texture2D texture2D, Vector2[] splits)
    {
        // 分别获取x,y两个数组
        float[] splitXs = new float[splits.Length + 2];
        float[] splitYs = new float[splits.Length + 2];
        splitXs[0] = 0;
        splitXs[splitXs.Length - 1] = spriteWidth;
        splitYs[0] = 0;
        splitYs[splitYs.Length - 1] = spriteHeight;
        for (int i = 0; i < splits.Length; i++)
        {
            splitXs[i + 1] = splits[i].x;
            splitYs[i + 1] = spriteHeight - splits[i].y;    // y轴坐标系倒转
        }
        // 对数组进行升序排序
        Sort<float> sort = new Sort<float>();
        sort.QuickSort(splitXs, 0, splits.Length);
        sort.QuickSort(splitYs, 0, splits.Length);
        // 分割物体
        for (int i = 0; i < splitXs.Length - 1; i++)
        {
            for (int j = 0; j < splitYs.Length - 1; j++)
            {
                float x1 = splitXs[i];
                float y1 = splitYs[j];
                float x2 = splitXs[i + 1];
                float y2 = splitYs[j + 1];
                float centerX = gameObject.transform.position.x - gameObject.transform.localScale.x / 2 + (x1 + x2) / (2 * spriteWidth);
                float centerY = gameObject.transform.position.y - gameObject.transform.localScale.y / 2 + (y1 + y2) / (2 * spriteHeight);
                Rect rect = new Rect(x1, y1, x2 - x1, y2 - y1);
                Sprite sprite = Sprite.Create(texture2D, rect, Vector2.zero);
                Vector2 position = new Vector2(centerX, centerY);
                fragments.Add(CreateFragment(sprite, position));
            }
        }
    }

    /// <summary>
    /// 在spriteRenderer区域内获取随机分割点。
    /// </summary>
    /// <returns>分割点数组。</returns>
    private Vector2[] RandomSplits()
    {
        System.Random random;
        Vector2[] splits = new Vector2[splitPoint];
        // 为了避免割点聚集,先分割区域,再于对应区域随机取点
        float spanX = spriteWidth / (2 * splitPoint + 1);
        float spanY = spriteHeight / (2 * splitPoint + 1);
        for (int i = 0; i < splitPoint; i++)
        {
            random = new System.Random(unchecked((int)System.DateTime.Now.Ticks) + seed);
            seed++;
            double x = random.NextDouble() * spanX + 2 * (i + 1) * spanX;
            random = new System.Random(unchecked((int)System.DateTime.Now.Ticks) + seed);
            seed++;
            double y = random.NextDouble() * spanY + 2 * (i + 1) * spanY;
            splits[i] = new Vector2((float)x, (float)y);
        }
        return splits;
    }

    /// <summary>
    /// 弹射一个碎片对象。
    /// </summary>
    /// <param name="fragment">碎片对象。</param>
    private void Ejection(GameObject fragment)
    {
        Vector2 start = fragment.transform.position;
        Vector2 end = gameObject.transform.position;
        Vector2 direction = end - start;
        fragment.GetComponent<Rigidbody2D>().AddForce(direction * forceMultiply, ForceMode2D.Impulse);
    }

    /// <summary>
    /// 创造一个碎片对象。
    /// </summary>
    /// <param name="sprite">碎片贴图。</param>
    /// <param name="position">碎片贴图位置。</param>
    /// <returns>碎片对象。</returns>
    private GameObject CreateFragment(Sprite sprite, Vector2 position)
    {
        GameObject fragment = new GameObject("Fragment");
        fragment.layer = LayerMask.NameToLayer(layerName);
        fragment.transform.position = position;
        fragment.AddComponent<SpriteRenderer>().sprite = sprite;
        // 可以将碎片视作刚体,这样会有与地形的碰撞效果
        fragment.AddComponent<Rigidbody2D>();
        fragment.AddComponent<BoxCollider2D>();
        fragment.AddComponent<FadeOut>().delaySecond = delaySecond;     // 添加淡出效果
        return fragment;
    }
    #endregion
}

  1. FadeOut类的代码已在上一节给出;
  2. 测试类ClickImage:
using UnityEngine;

public class ClickImage : MonoBehaviour
{
    public GameObject sprite = null;

    private void Update()
    {
        if (Input.GetMouseButtonUp(0))
        {
            sprite.GetComponent<Crasher>().Crash();
            sprite.SetActive(false);
        }
    }
}

附加内容

  • 为了单独封装,本项目内的碎片对象没有用对象池来回收,实际上,反复生成的垃圾对象,用对象池的效率比较高;
  • 不规则的Sprite也是可以破碎的,如下图所示:不规则的Sprite破碎
  • 11
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: Unity2D游戏特效是指在Unity引擎中利用2D技术实现的各种视觉效果。这些特效能够提升游戏的视觉表现力,使得游戏更加生动、有趣、具有冲击力。 首先,Unity2D游戏特效可以通过粒子系统实现各种效果,例如爆炸、火焰、烟雾、泡沫等等。通过调整粒子系统的参数,可以使得特效更逼真,更符合游戏情境。 其次,Unity2D游戏特效还可以通过动画制作来实现。使用Unity的动画编辑器,可以制作角色的动态动作、技能特效等。通过关键帧的设置和曲线插值等技术,使得动画更加流畅、逼真,并能给玩家带来更好的游戏体验。 此外,Unity2D游戏特效还可以通过材质和光照的调整来实现。通过设置材质的属性和贴图,可以使得游戏中的物体拥有更加真实的质感和细节。同时,利用光照技术可以给游戏场景增加更强的氛围,使得游戏的画面更加有层次感和立体感。 最后,Unity2D游戏特效还可以利用屏幕后处理效果来实现。通过使用像素运算和模糊等技术,可以对游戏画面进行各种特殊效果的处理,如模糊、扭曲、加深、融合等等。这样可以在不改变原始图像的情况下,增加画面的冲击力和独特感。 综上所述,Unity2D游戏特效游戏开发中具有非常重要的作用。通过合理运用粒子系统、动画制作、材质和光照调整以及屏幕后处理效果等技术,可以使得游戏的画面更加精彩、华丽,给玩家带来更好的游戏体验。 ### 回答2: Unity2D游戏特效是指在Unity引擎中使用2D图像和动画来增强游戏场景和角色的视觉效果。Unity2D游戏特效可以使游戏更加生动、丰富和吸引人。 在Unity2D实现游戏特效的一种常见方法是使用粒子系统。粒子系统通过在游戏场景中播放大量的小粒子来模拟各种自然和人工现象,例如火焰、爆炸、雨水、烟雾等等。粒子系统的参数可以通过调整颜色、大小、速度、角速度等属性来实现不同效果。可以通过在粒子系统上叠加多个效果来实现更复杂的特效,并可以通过代码控制粒子系统的行为,实现动态和交互性。 除了粒子系统,Unity2D还支持使用Shader(着色器)来创建游戏特效。Shader是一种在计算机图形中使用的程序,用于控制物体的材质和渲染方式。Unity2D的Shader可以实现各种效果,例如描边、扭曲、水波纹等。开发者可以使用自定义Shader来创建独特的游戏特效,并可以通过调整参数和使用代码实现特效的动态变化。 此外,Unity2D还提供了许多内置的特效工具和组件库,例如光影系统、特殊效果和动画效果等。开发者可以通过使用这些工具和组件来快速实现一些常见的特效,同时也可以通过引入第三方插件和资源来扩展和增强游戏特效的表现力。 总的来说,Unity2D游戏特效实现是通过使用粒子系统、Shader、内置工具和组件等来增强游戏场景和角色的视觉效果。通过合理运用这些特效,可以使游戏更加精彩、吸引人,并且提升玩家的游戏体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值