编写一个粒子光环
要求
参考 http://i-remember.fr/en 这类网站,使用粒子流编程控制制作一些效果, 如“粒子光环”
项目架构
软件版本
项目使用的开发软件为Unity 3D 2020.1.4f1c1。
文件组织

项目的资源文件夹包括Assets和Packages两个子文件夹。其中,Packages子文件夹存储了系统自带的一些包,在这个项目中并没有特别使用。而Assets文件夹则存储了这次游戏项目使用的资源,如场景,脚本等。Assets文件夹中的Scenes子文件夹存储了游戏的场景(这个游戏中只有一个场景)。而Scripts子文件夹存储了游戏中使用的脚本。
其中,项目的脚本包括如下文件:

项目地址
由于整个游戏文件夹相对过大,这里按照实验要求仅将Assets文件夹传到了公开的仓库上。仓库的链接为https://github.com/alphabstc/3D-Particle-System。新建一个Unity 3D项目,按照下面的指引将仓库内容导入,将脚本拖到对应的对象上,应该可以创建出一个可以正常运行的游戏。
游戏实现
项目内容说明
本项目完成了类似于 http://i-remember.fr/en 等网站的粒子光环的设计,实现了包含两个粒子往不同方向旋转的均匀子光环的粒子光环。另外实现了粒子颜色随位置和时间变化,外层光环的粒子向外方漂浮,以及鼠标点击使得内外光环交换位置的附加功能。
创建粒子系统对象
首先在场景的层次试图中创建empty对象ParticleHalo,然后在其下面创建两个子对象,分别命名为ClockwiseHalo和CounterclockwiseHalo,表示顺时针和逆时针旋转的两个光环,如下图所示:

接着,给这两个子对象添加粒子系统的组件:


思路与脚本设计
接着考虑通过脚本来控制上面的粒子系统组件,达到粒子光环的效果。接下来创建ParticleHalo.cs这一脚本文件。
首先定义一个数据结构particleData来存储粒子系统的相关参数,radius和angle相当于粒子的极坐标,分别表示粒子当前所在轨道的半径,以及当前所在位置与极坐标原点的连线到极轴的角度:
public class particleData {
public float radius = 0, angle = 0, time = 0;
public particleData(float radius, float angle, float time){//构造函数
this.radius = radius;//粒子轨道半径
this.angle = angle;//粒子角度(粒子当前所在位置与极坐标原点的连线到极轴的角度)
this.time = time;//时间,影响粒子漂浮时的轨道半径大小
}
}
之后定义一个粒子系统的类ParticleHalo,其继承于MonoBehaviour类,包含如下的字段,具体含义分析详见代码注释:
private ParticleSystem parSys;//对应粒子系统
private ParticleSystem.Particle[] particleArr; //存储粒子
private particleData[] particles;//存储对应粒子的参数
private float[] radius; //外圈粒子半径
private float[] collect_radius;//内圈粒子半径
private int tier = 15;//层数
private int time = 0;//时间
public Gradient colorGradient;//颜色梯度
public int particleNum = 30000;//粒子数目
public float size = 0.03f;//粒子大小
public float minRadius = 6.0f;//外圈最小半径
public float maxRadius = 12.0f;//外圈最大半径
public float collect_MaxRadius = 4.0f;//内圈最大半径
public float collect_MinRadius = 1.0f;//内圈最小半径
public bool clockwise = true; //是否顺时针旋转
public float speed = 1.5f; //速度
public float pingPong = 0.02f; //粒子处理间隔
public int isCollected = 0;//确定当前是内圈还是/外圈
上面的字段中包含内圈和外圈的最小半径,最大半径的内容,针对内圈和外圈也都有数据结构存储每个粒子所在的半径轨道,也就是其可以支持一个子光环切换到外圈或者内圈。而particleNum表示粒子数目,其数值越大,对计算机系统的性能要求越高,但也能产生更复杂精细的粒子系统效果。colorGradient颜色梯度则支持在粒子光环上产生绚丽的颜色渐变效果。
之后,在Start函数内完成上述字段的初始化操作,其实例化粒子系统的各个对象字段,并在最后调用RandomlySpread函数初始化各粒子的属性与位置:
void Start(){//初始化
particleArr = new ParticleSystem.Particle[particleNum];//创建粒子数组
particles = new particleData[particleNum];//创建粒子参数数组
radius = new float[particleNum];//创建浮点数数组
collect_radius = new float[particleNum];//创建浮点数数组
parSys = this.GetComponent<ParticleSystem>();//获得对应粒子系统
var main = parSys.main;//获得对应的主参数
main.startSpeed = 0; //设置初始速度
main.startSize = size; //设置初始大小
main.loop = false; //设置是否循环
main.maxParticles = particleNum; //设置最大粒子数目
parSys.Emit(particleNum); //发射粒子
parSys.GetParticles(particleArr);//获得粒子数组
RandomlySpread();//随机散播粒子
}
其中RandomlySpread函数将粒子随机散播在环形区域上,形成光环的效果,其将产生particleNum个粒子,对每个粒子计算出其所在内圈和外圈的半径,以及角度,时间,位置等参数,代码实现如下,具体分析详见代码注释:
void RandomlySpread(){
float midRadius = (maxRadius + minRadius) * 0.5;//计算平均半径
float minRate = UnityEngine.Random.Range(1.0f, midRadius / minRadius);//在1到midRadius / minRadius之间产生一个随机数
float maxRate = UnityEngine.Random.Range(midRadius / maxRadius, 1.0f);//在midRadius / maxRadius到1之间产生一个随机数
float collect_MidRadius = (collect_MaxRadius + collect_MinRadius) * 0.5;//计算平均半径
float collect_outRate = Random.Range(1.0f, collect_MidRadius / collect_MinRadius);;//在1到midRadius / minRadius之间产生一个随机数
float collect_inRate = Random.Range(collect_MaxRadius / collect_MidRadius, 1f);//在midRadius / maxRadius到1之间产生一个随机数
for (int i = 0; i < particleNum; ++i){ //对于每个粒子
float _radius = UnityEngine.Random.Range(minRadius * minRate, maxRadius * maxRate);//在minRadius * minRate到maxRadius * maxRate之间产生一个随机数 设置为当前半径
radius[i] = _radius;//设置半径
float _collect_radius = Random.Range(collect_MinRadius * collect_outRate, collect_MaxRadius * collect_inRate);//在minRadius * minRate到maxRadius * maxRate之间产生一个随机数 设置为当前半径
collect_radius[i] = _collect_radius;//设置半径
float angle = UnityEngine.Random.Range(0.0f, 360.0f);//随机获取角度
float theta = angle / 180 * Mathf.PI;//计算角度出弧度制下对应的值
float time = UnityEngine.Random.Range(0.0f, 360.0f);//随机获取时间
if (isCollected == 0) //当前为外圈
particles[i] = new particleData(_radius, angle, time);//设置为外圈参数
else//当前为内圈
particles[i] = new particleData(_collect_radius, angle, time);//设置为内圈参数
particleArr[i].position = new Vector3(particles[i].radius * Mathf.Cos(theta), 0f, particles[i].radius * Mathf.Sin(theta));//根据半径和角度计算出所处位置
}
parSys.SetParticles(particleArr, particleArr.Length);//设定粒子数组
}
注意到上面产生粒子时,先计算出半径平均值,然后计算出半径平均值相对最小半径和最大半径的比例,根据这两个比例计算出一个介于最小半径和最大半径之间的环形区域,再在该环形区域中均匀产生出粒子。这样就使得产生的粒子在光环中是均匀分布的,使得光环有着良好的视觉美观。如果产生粒子在光环中的分布不均匀,那么看起来效果会不好,缺少对称美,不是一种好的游戏效果。
目前,根据上面的代码可以显示一个两层的粒子光环,不过目前粒子光环颜色单一,也不会移动,也没有其他的特效。现在考虑给粒子光环增添更多的视觉和游戏效果。
为此,接着考虑在Update函数中进行关键效果的实现。Update函数中实现了粒子的旋转(支持根据需要顺时针和逆时针旋转)。粒子旋转是这样实现的:让每个粒子的(极坐标)角度在每次Update(每个渲染周期)都增加或减少(通过clockwise判断是顺时针还是逆时针旋转,进而决定是增加或减少)一个特定的数值,这样就实现了粒子绕光环中心旋转。为了让光环旋转更具有视觉效果,还可以设置一个与粒子编号相关的函数将粒子分为若干类,使得角度的变化量等于该函数的返回值,这样就实现了不同类的粒子以不同速度旋转。此外,Update函数还实现了内外圈光环的渐变切换效果。Update函数的具体实现如下,具体代码分析详见注释:
void Update (){//每个渲染周期
for (int i = 0; i < particleNum; i++) {//对于每个粒子
if (clockwise) //顺时针
particles[i].angle -= (i % tier + 1) * (speed / particles[i].radius / tier);//减少角度 角度变化量和i % tier的分类结果相关
else
particles[i].angle += (i % tier + 1) * (speed / particles[i].radius / tier);//增加角度 角度变化量和i % tier的分类结果相关
particles[i].angle = (360.0f + particles[i].angle) % 360.0f;//调整到0~360范围内
float theta = particles[i].angle / 180.0f * Mathf.PI;//计算出弧度制角度
if (isCollected == 1){//内圈
if (particles[i].radius > collect_radius[i]) //但该粒子当前实际处于外圈
particles[i].radius -= 16f * (collect_radius[i] / collect_radius [i]) * Time.deltaTime; //减少半径
else
particles[i].radius = collect_radius [i];//设置为内圈半径
}
else {//外圈
if (particles[i].radius < radius [i]) //但该粒子当前实际处于内圈
particles[i].radius += 16f * (collect_radius [i] / collect_radius [i]) * Time.deltaTime; //增加半径
else
particles[i].radius += Mathf.PingPong (particles[i].time / minRadius / maxRadius, pingPong) - pingPong / 2.0f;//外圈有漂浮效果 在外圈上按照一定的速度浮动
}
particleArr [i].position = new Vector3 (particles[i].radius * Mathf.Cos (theta), 0f, particles[i].radius * Mathf.Sin (theta));//根据极坐标计算出对应的直角坐标
}
changeColor ();//修改颜色
parSys.SetParticles(particleArr, particleArr.Length);//重新设置各个粒子
}
changeColor函数则实现了光环上颜色的渐变效果,其原理是让每个粒子的颜色等于当前游戏时间与粒子极坐标角度累加后平移区间到0~1数值范围内,再根据该数值计算出对应的颜色。其实现如下,具体分析详见代码注释:
void changeColor(){
float colorValue;
for (int i = 0; i < particleNum; i++){//遍历每个粒子
colorValue = (Time.realtimeSinceStartup - Mathf.Floor(Time.realtimeSinceStartup));//颜色随着时间变化
colorValue += particles[i].angle/360;//颜色也和当前位置极坐标角度有关
while (colorValue > 1) colorValue--;//调整到0~1范围内
particleArr[i].startColor = colorGradient.Evaluate(colorValue);//设置颜色
}
}
之后,再实现OnGUI函数如下,其在鼠标点击时,设置isCollected属性变为对偶值,使得内圈和外圈交换:
void OnGUI(){
if(Input.GetMouseButtonDown(0)){//鼠标点击
isCollected = 1 - isCollected;//修改是内圈还是外圈
}
}
这样就完成了粒子光环的脚本设计。
最后,将ParticleHalo.cs脚本分别拖到到上面的两个子光环上。并设置两个子光环的脚本相应初始参数如下:


调整摄像头
之后调整摄像头的位置和姿态,设置摄像头参数如下,使得其可以以比较好的视角看见游戏场景,且有着黑色的背景底色。

游戏效果

4355

被折叠的 条评论
为什么被折叠?



