子弹系统和粒子系统比较类似,为了创建出五花八门的子弹,例如追踪,连续继承,散弹等,需要一个拥有众多参数的子弹生成器,这里叫它Shooter好了。
Shooter负责把玩各类子弹造型和参数,创建出子弹,创建完了之后接下来就交给子弹自己来管理自己了。
所以,一个子弹系统包含:
1.ShooterSystem类
一个能生成各种类型子弹的发射器。
2.Bullet类
按照给定的初始参数不断向前飞行的子弹个体。
先思考每一个单独的子弹需要有哪些物理参数:
1 //目标
2 public GameObject Target { get; set; }
3 //瞬时速度
4 public float Velocity { get; set; }
5 //剩余生命周期
6 public float LifeTime { get; set; }
7 //角速度
8 public float Palstance { get; set; }
9 //线性加速度
10 public float Acceleration { get; set; }
11 //最大速度
12 public float MaxVelocity { get; set; }
这些参数不需要子弹自己来配置,而是交给把玩它们的Shooter来进行,但是子弹自身需要知道这些参数。
其中指得一提的是角速度,正常的子弹是没有追踪功能的,生成之后就只能自动向前飞,但一旦设置了子弹的目标后,子弹就必须根据角速度转向目标位置的向量,保证自己的前向能尽快和目标向量对齐;而这一对齐的过程,就需要用角速度来描述。
子弹在生命周期到了之后要自动销毁,因为它经常反复创建和销毁,最好使用对象池来进行这一过程:
调用如下:
1 public IEnumerator AutoRecycle()
2 {
3 yield return new WaitForSeconds(LifeTime);
4 ObjectPool.Instance.RecycleObj(gameObject);
5 }
子弹每一帧的状态都会有所变化,例如位置,速度的更新,向前运行的方向的更新等:
1 private void Update()
2 {
3 float deltaTime = Time.deltaTime;
4 //由当前子弹位置指向目标位置的向量,记为瞬时偏移向量
5 Vector3 offset = (Target.transform.position - transform.position).normalized;
6 //子弹的当前前进方向与瞬时偏移向量之间的夹角
7 float angle = Vector3.Angle(transform.forward, offset);
8 //夹角除以角速度计算需要转到相同方向所需要的总时间
9 float needTime = angle*1.0f / Palstance;
10 //插值运算出当前帧的前向方向向量,也即是需要偏移的角度
11 transform.forward = Vector3.Lerp(transform.forward, offset, deltaTime / needTime).normalized;
12 //处理线性加速度对于速度的增量
13 if (Velocity < MaxVelocity)
14 {
15 Velocity += deltaTime * Acceleration;
16 }
17 //按当前速度向前移动一帧的距离,赋值给当前位置
18 transform.position += transform.forward * Velocity * deltaTime;
19 }
如果不想让子弹追踪,也很简单,把角速度传为0即可,float除数为0也是没有问题的。
子弹生成器主要是创建子弹,所以需要包含子弹类的所有参数,除此之外,还需要有一些其他的参数:
1 public bool bAuto = false;
2
3 public GameObject bulletPrefab;
4 //子弹目标
5 public GameObject target;
6 //初速度
7 public float velocity = 0f;
8 //加速度
9 public float acceleration = 30f;
10 //总生命周期
11 public float lifeTime = 3f;
12 //初始方向
13 public Vector2 direction = Vector2.zero;
14 //最大速度
15 public float maxVelocity = 600;
16 //角速度
17 public float palstance = 120;
18 //角度波动范围
19 public float angelRange = 0f;
20 //延迟
21 public float delay = 1f;
22 //是否循环
23 public bool bLoop = false;
24 //时间间隔
25 public float timeCell = .1f;
26 //生成数量
27 public int count = 1;
28 //伤害
29 public float damage;
30 //碰撞类型
31 public CollisionType collisionType;
32 //是否有子系统
33 public bool bChildShooter = false;
34 //子系统是谁
35 public GameObject childShooter;
初始方向就是子弹生成后的前向方向,如果想制造散弹效果,则子弹就需要在一定的角度波动范围内生成前向方向,但生成的位置依然是统一的。
生成器还需要能循环生成子弹,能够在生成的子弹飞行过程中继续生成不一样效果的分裂子弹,所以还需要子系统,子系统和父系统可以写为同一个生成器类。需要注意的就是,子系统的生命周期需要依赖父系统生成的子弹的生命周期。
生成单个子弹的方法:
1 private void Creat(Transform parent)
2 {
3 //从对象池中取对象生成到指定物体下,复位坐标
4 var ins = ObjectPool.Instance.GetObj(bulletPrefab, parent.transform);
5 ins.transform.ResetLocal();
6
7 //对子弹的属性赋值
8 var bullet = ins.GetComponent<Bullet>();
9 bullet.Target = target;
10 bullet.Velocity = velocity;
11 bullet.Acceleration = acceleration;
12 bullet.LifeTime = lifeTime;
13 bullet.MaxVelocity = maxVelocity;
14 bullet.Palstance = palstance;
15
16 //确定子弹生成方向的范围,默认Z轴正方向为子弹飞行方向
17 float x = Random.Range(direction.x - angelRange / 2, direction.x + angelRange / 2);
18 float y = Random.Range(direction.y - angelRange / 2, direction.y + angelRange / 2);
19 bullet.transform.localEulerAngles = new Vector3(x, y, 0);
20
21 parent.DetachChildren();
22
23 //开启子弹自动回收
24 bullet.StartCoroutine(bullet.AutoRecycle());
25
26 //判断子生成器并自动运行
27 if (bChildShooter)
28 {
29 var cscs = childShooter.GetComponent<ShooterSystem>();
30 if (lifeTime > cscs.delay)
31 StartCoroutine(cscs.AutoCreat(bullet.transform, this));
32 else
33 Debug.Log("子发射器延迟时间设置有误!");
34 }
35 }
对于子生成器来说,它也同样可能拥有自己的子生成器,在AutoCreat的方法中需要传递它的父生成器是谁,默认情况下为空:
1 IEnumerator AutoCreat(Transform parent, ShooterSystem parShooter = null)
2 {
3 yield return new WaitForSeconds(delay);
4 if (bLoop)
5 {
6 if (parShooter != null)
7 {
8 //子生成器需要计算循环的次数,父生成器则是无限循环
9 int loopCount = (int)((parShooter.lifeTime - delay) / timeCell);
10 for (; loopCount > 0; loopCount--)
11 {
12 //每次循环生成的子弹数量
13 for (int i = 0; i < count; i++)
14 Creat(parent);
15 yield return new WaitForSeconds(timeCell);
16 }
17 }
18 else
19 {
20 for (; ; )
21 {
22 for (int i = 0; i < count; i++)
23 Creat(parent);
24 yield return new WaitForSeconds(timeCell);
25 }
26 }
27 }
28 else
29 {
30 for (int i = 0; i < count; i++)
31 Creat(parent);
32 }
33 }
有关伤害判断和碰撞检测不在此篇讨论范围内。
2019年12月12日更新:
增加以下几个功能:
1.可以控制子弹仅在单轴向的角度范围内散射,比如有时想让子弹只在同一个平面内散射,而不是在三维空间中。
2.可以控制子弹在散射范围内平均分布,而不是仅能随机分布。
3.可以控制子弹在非循环发射状态下按照固定时间间隔先后发射,比如追踪导弹一发发有序射击。
在此之前,先优化子弹中的一个小问题,子弹类的Update方法中,仅当存在追踪目标且角速度大于零时追踪目标:
1 private void Update()
2 {
3 float deltaTime = Time.deltaTime;
4 if (Target != null && Palstance > 0)
5 {
6 //由当前子弹位置指向目标位置的向量,记为瞬时偏移向量
7 Vector3 offset = (Target.transform.position - transform.position).normalized;
8 //子弹的当前前进方向与瞬时偏移向量之间的夹角
9 float angle = Vector3.Angle(transform.forward, offset);
10 //夹角除以角速度计算需要转到相同方向所需要的总时间
11 float needTime = angle * 1.0f / Palstance;
12 //插值运算出当前帧的前向方向向量,也即是需要偏移的角度
13 transform.forward = Vector3.Lerp(transform.forward, offset, deltaTime / needTime).normalized;
14 }
15 //处理线性加速度对于速度的增量
16 if (Velocity < MaxVelocity)
17 {
18 Velocity += deltaTime * Acceleration;
19 }
20 //按当前速度向前移动一帧的距离,赋值给当前位置
21 transform.position += transform.forward * Velocity * deltaTime;
22 }
下面开始实现前面的几个功能:
定义可选轴向,理论上只要绕两个方向的轴向就可以定义三维空间中的任何一个方向,这里将Z轴作为初始的前进方向因此不对Z轴作任何操作和改变。
1 public enum AngelRangeAxis
2 {
3 //仅在绕Y轴的平面上,也即是X-Z平面
4 RYAxis,
5 //仅在绕X轴的平面上,也即是Y-Z平面
6 RXAxis,
7 //三维空间中
8 BothXY
9 }
在ShooterSystem类中增加定义以下属性:
1 //是否固定单位角度
2 public bool bFixedAngel = false;
3 //单数量时间间隔
4 public float EachCountDur = 0f;
5 //计算得出的固定单位角度
6 private float FixAngel;
7 //范围轴向设置
8 public AngelRangeAxis RangeAxis;
在Start方法中根据一次发射数量计算单位角度:
1 if (bFixedAngel)
2 {
3 FixAngel = AngelRange / (Count - 1);
4 }
在Creat方法中增加参数——当前创建的子弹索引idx,默认值为-1,可以不传递该参数,当传递该参数时,用于计算每一子弹在范围内应处于的角度:
1 //确定子弹生成方向的范围,默认z轴正方向为子弹飞行方向
2 switch (RangeAxis)
3 {
4 case AngelRangeAxis.RYAxis:
5 bullet.transform.localEulerAngles = new Vector3(Direction.x, GetLocalEulerAxis(Direction.y, idx), 0);
6 break;
7 case AngelRangeAxis.RXAxis:
8 bullet.transform.localEulerAngles = new Vector3(GetLocalEulerAxis(Direction.x, idx), Direction.y, 0);
9 break;
10 case AngelRangeAxis.Both:
11 bullet.transform.localEulerAngles = new Vector3(GetLocalEulerAxis(Direction.x, idx), GetLocalEulerAxis(Direction.y, idx), 0);
12 break;
13 }
其中方法GetLocalEulerAxis定义如下,主要用于确定轴向的最终值(无论是固定角度还是随机):
1 private float GetLocalEulerAxis(float dirAxis,int idx)
2 {
3 if (bFixedAngel)
4 return dirAxis- AngelRange / 2 + FixAngel * idx;
5 else
6 return Random.Range(dirAxis - AngelRange / 2, dirAxis + AngelRange / 2);
7 }
在AutoCreat协程中的非循环生成条件中进行如下修改:
1 for (int i = 0; i < Count; i++)
2 {
3 if (bFixedAngel)
4 Creat(parent, i);
5 else
6 Creat(parent);
7 yield return new WaitForSeconds(EachCountDur);
8 }
修改后可以发射出类似于这样的追踪导弹:
或者这样同一平面内的等间距子弹: