先上实际效果
如上图所示,效果很直观,即原始的Sprite图像的破碎组件触发后,会将原图碎裂成无数小块,且使之炸裂。那么,要实现这个功能就有如下几点需求:
- 对于任意大小形状的Sprite,仅通过一个方法就能使其破碎;
- 尽可能的保证碎片的形状大小具有随机性,但是碎片不能太大,否则不美观;
- 触发完之后要让碎片炸开来;
- 考虑到复用性,要封装成一个组件,以便与工程解耦。
一、Sprite随机分割点生成
我们依然拿原图举例子。随机裁切的本质,即是在Sprite的矩形框内,随机找分割点,并对分割的轴做垂线,重新生成四个新的矩形区域,如下图所示:
只不过,用完全随机的方式来生成多个切割点的时候,如果两个点的坐标比较相近,那么实际效果就会很不美观(有些地方特别大,有些地方特别小),如下图:
所以,我们的目的是要分割点均匀的随机(没错,就是有前提的随机);
举个例子:如果我们希望生成9张碎裂后的子图片,则需要有2个分割点来分割图像,即N个分割点,生成(N+1)(N+1)张子图,可以采取如下图的区域划分方法。
- 先将区域划分为5X5的区域(即(2N+1)*(2N+1));
- 设左上方的子区域为S00,则于主对角线上,所有i和j坐标均为奇数的区域Sij内生成随机数(黄点所在区域);
- 对随机数生成的坐标进行升序排序;
- 从左上角的点依次分割至左下角。
算法如上所述那样,通过区域划分法,在对应子区域内生成分割点,即可以保证随机性,又不会使分割效果太丑。那么,生成了随机分割点之后,我们应该如果去分割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
}
完整代码
排序算法Sort类:
using System;
///
/// 排序算法类。 /// public class Sortwhere T : IComparable { #region 基础公有方法 /// /// 数组快速排序。 /// /// 待排序数组。 /// 排序起点。 /// 排序终点。 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 静态私有方法 ////// 泛型对象比较大小。 /// /// 待比较对象。 /// 待比较对象。 ///大于0则前者的值更大,小于0则反之,等于0则二者的值相等。 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 }核心代码Crasher组件类:
using System.Collections.Generic; using UnityEngine;
///
/// 物体分裂效果组件类。 /// 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 Listfragments = new List (); // 碎片对象列表 #endregion #region 功能方法 /// /// 对对象执行粉碎特效。 /// 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]); } ////// 根据割点获取所有碎片对象。 /// /// 原始对象的纹理。 /// 割点列表。 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轴坐标系倒转 } // 对数组进行升序排序 Sortsort = new Sort (); 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)); } } } /// /// 在spriteRenderer区域内获取随机分割点。 /// ///分割点数组。 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; } ////// 弹射一个碎片对象。 /// /// 碎片对象。 private void Ejection(GameObject fragment) { Vector2 start = fragment.transform.position; Vector2 end = gameObject.transform.position; Vector2 direction = end - start; fragment.GetComponent().AddForce(direction * forceMultiply, ForceMode2D.Impulse); } /// /// 创造一个碎片对象。 /// /// 碎片贴图。 /// 碎片贴图位置。 ///碎片对象。 private GameObject CreateFragment(Sprite sprite, Vector2 position) { GameObject fragment = new GameObject("Fragment"); fragment.layer = LayerMask.NameToLayer(layerName); fragment.transform.position = position; fragment.AddComponent().sprite = sprite; // 可以将碎片视作刚体,这样会有与地形的碰撞效果 fragment.AddComponent (); fragment.AddComponent (); fragment.AddComponent ().delaySecond = delaySecond; // 添加淡出效果 return fragment; } #endregion } FadeOut类的代码已在上一节给出;
测试类ClickImage:
using UnityEngine;
public class ClickImage : MonoBehaviour { public GameObject sprite = null; private void Update() { if (Input.GetMouseButtonUp(0)) { sprite.GetComponent
().Crash(); sprite.SetActive(false); } } }
附加内容
- 为了单独封装,本项目内的碎片对象没有用对象池来回收,实际上,反复生成的垃圾对象,用对象池的效率比较高;
- 不规则的Sprite也是可以破碎的,如下图所示: