Our team recently researched the special effects of “Flame Rain” and found many other developers’ solutions from the Internet. They were not very satisfactory for us, mainly because they could not meet the requirements of randomness and precise control at the same time. Finally, we decided to develop a component by ourselves.

We make use of the “machine gun” component which we develop previously. Machine guns can aim at targets and emit particle effects. The related API and prefabs support multiple targets, bullet prefabs, and the gun body can be rotated.

Our idea is to use many “machine guns” to build a “cloud”. This “cloud” can cause a regional “ball rain” attack on the ground.
Of course, this cloud is virtual, and it is not necessary to use render in the scene, it is only used to deploy machine guns. The machine gun is also invisible.

Here is the demo scene we used to verify the bullet cloud function during development:

Because the bullet cloud is usually invisible in the actual scene, you can deploy a bullet cloud anywhere. However, considering the time of flight of the particles, you need to decide the height of the cloud yourself. We implemented it as a well-encapsulated component. You can configure it with any number of raindrops to hit the target precisely (of course, there can be multiple targets), the amount of hit you want to happend on each target, which bullets you want to use. And, there are many other bullets which looks like randomly drop, but actually they are configured by us.

When deploying the bullet cloud to your own scene, you need to pay attention to check the particle effects used as bullets, so you can ensure that there is no layer which can collide with your target.

We applied the previously implemented particles of various projectiles as cloud bullets, which triggered explosions based on physical collisions. At the same time, we determine whether a certain rain process of the bullet cloud is completed based on the collision. This API is necessary for the design of the skill system. Because we need to know when the skills we have released are over.

Of course, if you want to use purely mathematical methods to determine whether the bullet hits the target, this part needs to be implemented by yourself. Our bullet cloud can’t do that yet.

Using this bullet cloud to achieve the effect of “Flame Rain”, we are quite satisfied, because it has achieved both randomly effect and accuratly control on the damage. Because many of our game’s skill look randomly, but their damage amount is fixed.

The implementation logic is still quite complicated, try it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
void EnsurePreciseHitDrop(int _preciseHitCount = -1)
{
PreciseHitCount = _preciseHitCount == -1 ? preciseHitCountPreset : _preciseHitCount;

if (PreciseHitCount > 0 && Targets != null && Targets.Length > 0)
{
PreciseHitDrop = new Aimer[PreciseHitCount];

if (PreciseHitCount == 1)
{
if (Targets.Length == 1)
{
PreciseHitDrop[0] = CenterDrop;
preciseDropMaxIndexInAllDrop = 0;
}
else
{
Vector3 target = Targets[Random.Range(0, Targets.Length)];

PreciseHitDrop[0] = AllDrop[1];
PreciseHitDrop[0].transform.position = DropMoveToAimAtTarget(target);

preciseDropMaxIndexInAllDrop = 1;
}
}
else if (PreciseHitCount > 1 && PreciseHitCount <= DropCount)
{
if (Targets.Length == 1)
{
PreciseHitDrop[0] = CenterDrop;
for (int i = 1; i < PreciseHitCount; i++)
{
PreciseHitDrop[i] = AllDrop[i];
PreciseHitDrop[i].transform.position = CenterDrop.transform.position;
}

preciseDropMaxIndexInAllDrop = PreciseHitCount - 1;
}
else if (Targets.Length > 1)
{
int targetCount = Targets.Length;
if (EquallyAssignPreciseHit)
{
int quota = PreciseHitCount / targetCount;
int remain = PreciseHitCount % targetCount;

int allDropIndex = 1;
int preciseDropIndex = 0;

for (int targetIndex = 0; targetIndex < targetCount; targetIndex++)
{
for (int i = 0; i < quota; i++)
{
PreciseHitDrop[preciseDropIndex] = AllDrop[allDropIndex];
PreciseHitDrop[preciseDropIndex].transform.position
= DropMoveToAimAtTarget(Targets[targetIndex]);

allDropIndex++;
preciseDropIndex++;
}
}

if (remain > 0)
{
for (int j = 0; j < remain; j++)
{
Vector3 target = Targets[Random.Range(0, Targets.Length)];

PreciseHitDrop[preciseDropIndex] = AllDrop[allDropIndex];
PreciseHitDrop[preciseDropIndex].transform.position
= DropMoveToAimAtTarget(target);

allDropIndex++;
preciseDropIndex++;
}
}
}
else
{
int allDropIndex = 1;
int preciseDropIndex = 0;

for (int i = 0; i < PreciseHitCount; i++)
{
Vector3 target = Targets[Random.Range(0, Targets.Length)];

PreciseHitDrop[preciseDropIndex] = AllDrop[allDropIndex];
PreciseHitDrop[preciseDropIndex].transform.position
= DropMoveToAimAtTarget(target);

allDropIndex++;
preciseDropIndex++;
}
}

preciseDropMaxIndexInAllDrop = PreciseHitCount;
}
}
else
{
preciseDropMaxIndexInAllDrop = DropCount - 1;

throw new System.InvalidOperationException("Precise hit count exceed total drop count.");
}
}
}