特点:鼠标在圈外时,粒子作内外往返以及圆周运动,整体在径向上维持正态分布;鼠标移到圈内时,光环收缩并加速往返。
程序源码:
粒子光环
文章最后也附上了完整的脚本代码。
注意,程序中还包括一个使用了材质的爆炸光环,一个粒子海洋。其中粒子海洋是用代码编写粒子行为的。感兴趣的也可以看一下(代码是copy别人的,主要是使用到了柏林噪声)。
这里主要介绍如何用脚本生成一个可交互的粒子光环。
程序结构
其中Empty_2下的粒子系统就是粒子光环。
由于是脚本控制粒子行为,因此这里需要把粒子系统中的粒子持续时间(Start Lifetime)调大些,然后将速度(Rate over Time)调整为0 。
为了让粒子光环更加随机,这里还可以调整粒子的大小(Start Size)曲线。
以上是挂载在粒子系统的父对象(空物体)上的脚本中可以设置的参数。在脚本挂载后,默认参数就会出现,需要调整的是粒子的渐变颜色(Gradient)。
这里解释一下参数的意义:
- ParticleNum,粒子生成数量。
- RadiusIn,光环内圈,也是碰撞盒的半径(因此调整时需要一起调)。
- RadiusOut,光环外圈,这两个在计算正态分布(即粒子初始半径)时需要用到。
- RadiusOut_2,光环收缩后的外圈。
- Speed,粒子作圆周运动的速度(分运动)。
- Speed_2,粒子作内外往返运动时的速度(鼠标在圈外)。
- Speed_3,粒子作内外往返运动时的速度(鼠标在圈内)。
- StdDev,正态分布的标准差,用于决定光环的扩散范围(即环的厚度)。
另外就是碰撞盒了。这里由于采用射线的方式来判断鼠标是否移动到圈内,因此需要一个碰撞盒,该组件同样挂载在空的父对象上。
程序设计
粒子生成
这个算是粒子系统编程的基础了。大概来说就是以下几个步骤:
- 创建一个存储粒子的数组
particles = new ParticleSystem.Particle[particleNum];
- 设置粒子系统最大粒子数
ParticleSystem.MainModule mainPSystem = particleSystem.main; mainPSystem.maxParticles = particleNum;
- 发射指定数量的粒子(生成粒子)
particleSystem.Emit(particleNum);
- 获取这些粒子的引用以操作粒子
particleSystem.GetParticles(particles);
- 在修改粒子属性后还需要更新粒子
particleSystem.SetParticles(particles, particles.Length);
粒子分布
为了让粒子在光环内的分布具有新意,这里参照了他人博客中的想法,利用正态分布来生成粒子的初始半径(以粒子系统为圆心),生成方法为:Box-Muller
private float GetNormalDistribution(float min, float Dev)
{
float u1 = Random.Range(0f, 1f);
float u2 = Random.Range(0f, 1f);
float r = Mathf.Sqrt(-2 * Mathf.Log(u1));
float sita = u2 * Mathf.PI * 2;
return min + Dev * r * Mathf.Sin(sita);
}
参数为生成正态分布的均值和标准差(正态分布的定义应该还记得吧)。
返回的半径将在初始化时被存储到一个大小为粒子数量的数组中,并设置粒子的初始位置。
float r = GetNormalDistribution((radiusOut + radiusIn) * 0.5f, stdDev);
这里就使用到了 radiusIn 和 radiusOut 。
当然除了初始半径,还需要一个初始角度才可以得到一个位置。这里直接利用随机函数生成。
float angle = Random.Range(0f, 360f);
设置粒子坐标:
particles[i].position = new Vector3(r * Mathf.Cos(rad), r * Mathf.Sin(rad), 0);
粒子运动
圆周运动(分运动)
粒子的圆周运动在编程中的实现就是逐帧修改(递增或递减)粒子的角度(angle)。改变的大小利用 Speed 参数来控制。为了让粒子的运动看起来更随机,这里设置了五个速度梯度以及正反两个运动方向,分别以粒子在数组中的位置来标识:
float num = speed * (i % 5 + 1) * Time.deltaTime;
if (i%2 == 0)
particleAngle[i] += num;
else
particleAngle[i] -= num;
if (particleAngle[i] > 360)
particleAngle[i] -= 360;
else if (particleAngle[i] < 0)
particleAngle[i] += 360;
往返运动(分运动)
粒子在往返运动的同时需要保持初始的正态分布,这一点我决定利用:让粒子在以自己的初始半径和另外一个随机粒子的初始半径为边界的范围内往返运动。
为了实现这一点,需要:
- 一个与初始半径数组一一对应的随机半径数组(也就是说需要重排半径数组)
- 一个控制粒子何时往返的标志(一个bool类型数组)
对于第一点,只需要随机排列半径数组内的元素就可以达到生成粒子往返边界的目的。这里使用 洗牌算法 (原理很简单,感兴趣可以去搜索一下)。
private float[] GetRandomArray(float[] arr)
{
float[] newArr = new float[arr.Length];
int len = arr.Length;
int num;
float temp;
for (int i = 0; i < len; i++)
{
newArr[i] = arr[i];
}
for (int i = 0; i < len; i++)
{
num = Random.Range(0, len);
temp = newArr[num];
newArr[num] = newArr[i];
newArr[i] = temp;
}
return newArr;
}
这个随机排列的半径数组同样是在初始化时生成。为了让后面使用简便,这里还对这两个数组的元素做了大小比较,也就是说,particleRadius_1
内的半径一定比particleRadius_2
中的小(作为起始点)。
对于第二点,就是创建一个粒子数量大小的bool类型数组,然后初始化为false(默认就是false)。在Update
中,如果粒子对应的标志为true,则向外移动(当前半径加),false向内移动(当前半径减)。
一旦粒子超出边界,则对应标志更改以控制粒子返回。
float num_2 = curSpeed * (i % 5 + 1) * Time.deltaTime;
if (direction[i])
particleRadius[i] += num_2;
else
particleRadius[i] -= num_2;
if (particleRadius[i] > endRadius)
direction[i] = false;
else if (particleRadius[i] < startRadius)
direction[i] = true;
交互事件
检测鼠标移动
这里很简单,就是利用射线的方式,一旦射线击中tag为 button 的物体(这里就是父物体上的球形碰撞盒),就会将标志 isHover 设置为true。
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit) && hit.collider.gameObject.CompareTag("button"))
{
isHover = true;
}
else
{
isHover = false;
}
控制粒子状态
光环收缩在这里可以实现为修改粒子往返的边界和速度(startRadius,endRadius, curSpeed)。
if (isHover)
{
startRadius = radiusIn;
endRadius = radiusOut_2;
curSpeed = speed_3;
}
else
{
startRadius = particleRadius_1[i];
endRadius = particleRadius_2[i];
curSpeed = speed_2;
}
至此就可以逐帧修改粒子位置了:
float rad = particleAngle[i] / 180 * Mathf.PI;
float rs = particleRadius[i];
particles[i].position = new Vector3(rs * Mathf.Cos(rad), rs * Mathf.Sin(rad), 0);
最后可以为光环增加一个渐变色彩:
public Gradient gradient;
particles[i].startColor = gradient.Evaluate(rs-radiusIn-0.5f);
完整脚本代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Particle_1 : MonoBehaviour
{
public new ParticleSystem particleSystem;
public int particleNum = 3000;
public float radiusIn = 8f;
public float radiusOut = 12f;
public float radiusOut_2 = 10f;
public float speed = 1f;
public float speed_2 = 0.2f;
public float speed_3 = 1f;
public float stdDev = 1.5f;
public Gradient gradient;
private ParticleSystem.Particle[] particles;
private float[] particleRadius;
private float[] particleAngle;
private float[] particleRadius_1;
private float[] particleRadius_2;
private bool[] direction;
private bool isHover;
private void Start()
{
isHover = false;
particles = new ParticleSystem.Particle[particleNum];
particleRadius = new float[particleNum];
particleRadius_1 = new float[particleNum];
particleAngle = new float[particleNum];
direction = new bool[particleNum];
ParticleSystem.MainModule mainPSystem = particleSystem.main;
mainPSystem.maxParticles = particleNum;
particleSystem.Emit(particleNum);
particleSystem.GetParticles(particles);
for(int i = 0; i < particleNum; i++)
{
float angle = Random.Range(0f, 360f);
float r = GetNormalDistribution((radiusOut + radiusIn) * 0.5f, stdDev);
float rad = angle / 180 * Mathf.PI;
particleRadius[i] = r;
particleRadius_1[i] = r;
particleAngle[i] = angle;
particles[i].position = new Vector3(r * Mathf.Cos(rad), r * Mathf.Sin(rad), 0);
if (i % 2 == 0)
direction[i] = true;
else
direction[i] = false;
}
particleSystem.SetParticles(particles, particles.Length);
particleRadius_2 = GetRandomArray(particleRadius);
for(int i = 0; i < particleNum; i++)
{
if(particleRadius_1[i] > particleRadius_2[i])
{
float temp = particleRadius_2[i];
particleRadius_2[i] = particleRadius_1[i];
particleRadius_1[i] = temp;
}
}
}
float startRadius, endRadius, curSpeed;
private void Update()
{
for(int i = 0; i < particleNum; i++)
{
if (isHover)
{
startRadius = radiusIn;
endRadius = radiusOut_2;
curSpeed = speed_3;
}
else
{
startRadius = particleRadius_1[i];
endRadius = particleRadius_2[i];
curSpeed = speed_2;
}
float num = speed * (i % 5 + 1) * Time.deltaTime;
float num_2 = curSpeed * (i % 5 + 1) * Time.deltaTime;
if (i%2 == 0)
particleAngle[i] += num;
else
particleAngle[i] -= num;
if (direction[i])
particleRadius[i] += num_2;
else
particleRadius[i] -= num_2;
if (particleAngle[i] > 360)
particleAngle[i] -= 360;
else if (particleAngle[i] < 0)
particleAngle[i] += 360;
if (particleRadius[i] > endRadius)
direction[i] = false;
else if (particleRadius[i] < startRadius)
direction[i] = true;
float rad = particleAngle[i] / 180 * Mathf.PI;
float rs = particleRadius[i];
particles[i].position = new Vector3(rs * Mathf.Cos(rad), rs * Mathf.Sin(rad), 0);
particles[i].startColor = gradient.Evaluate(rs-radiusIn-0.5f);
}
particleSystem.SetParticles(particles, particles.Length);
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit) && hit.collider.gameObject.CompareTag("button"))
{
isHover = true;
}else
{
isHover = false;
}
}
private float GetNormalDistribution(float min, float Dev)
{
float u1 = Random.Range(0f, 1f);
float u2 = Random.Range(0f, 1f);
float r = Mathf.Sqrt(-2 * Mathf.Log(u1));
float sita = u2 * Mathf.PI * 2;
return min + Dev * r * Mathf.Sin(sita);
}
private float[] GetRandomArray(float[] arr)
{
float[] newArr = new float[arr.Length];
int len = arr.Length;
int num;
float temp;
for(int i = 0; i < len; i++)
{
newArr[i] = arr[i];
}
for(int i = 0; i < len; i++)
{
num = Random.Range(0, len);
temp = newArr[num];
newArr[num] = newArr[i];
newArr[i] = temp;
}
return newArr;
}
}