Update functions, continued

<<< PREV NEXT >>>

We are continuing the topic of update functions. After whetting our appetite in the first half of the chapter, we’re about get into the truly wild territory of particle functions and particles that spawn other particles!

The first section is on the topic of particle functions, which are specialized, built-in functions that operate on particles. By leveraging them, you can make particles interact with their surroundings or otherwise show unique behavior that is either impossible or very difficult to achieve in any other way.

Next up, we embark on the topic of making particles spawn (or birth) other particles. This is just a consequence of allowing particles to do pretty much anything you want them to in update functions. However, especially with this feature, you can shoot yourself in the foot and make the game crash or severely lag. For this reason, the section dedicated to it first starts out with heavy warnings to be careful with this spectacular, but potentially dangerous aspect of particle scripts.

Towards the end, we explore more specific topics related to performance of your particle scripts, the strategies involved in making optimized scripts, as well as the tools provided by the plugin, that are at your disposal to monitor and debug particle scripts.

Sections

        1. Particle functions
        2. Animated particles
        3. Simple colliding particles
        4. Homing and following particles
        5. Particles that spawn other particles
        6. Immortal particles
        7. Remarks about performance
        8. The debugging interface


Particle functions and methods

As we have seen in the previous half of the chapter, we can already do plenty of different things with particles in update functions. Manipulating particles frame by frame is certainly a lot of fun. However, the usefulness of this feature would not be that great, if that’s all there was to update functions. After all, I promised that thanks to update functions, particles can not only be manipulated in real time, but can also be instructed to interact with their environment. How do we make the particles interactible, though? None of the particle properties described to us seem like can help in achieving that.

Indeed, particle fields alone will not help in this task. What will help is particle functions.

What are particle functions?

Simply put, they are built-in functions defined in the plugin API, which operate on particles.

You may be thinking we have already seen particle functions in action before. The init and update functions are functions that work with particles, after all? Indeed, they are functions that manage particles, but these functions are written by ourselves, in Lua. Init brings a new particle into the world, so it’s more akin to a particle spawner. Update is much closer to a particle function, since it operates on already existing particles. But init and update are both functions defined by us, the developer of the effect.

Particle functions, on the other hand, are functions predefined in the plugin DLL, available through the particle scripting API.

What do particle functions do?

They perform specific operations on particle instances.

What kind of operations are we talking about? Well, pretty much anything you can think of. You want a particle to collide with room geometry? There is a particle function for that. You want particles to respond to the wind of an outside room? There’s a particle function for that as well. You want a particle to track a specific moveable and accelerate towards its direction, behaving like a homing missile? You bet, there’s a particle function for it in the API. You want a particle to detect when it collided with the moveable it was homing onto? There’s yet another particle function for it. Certain particle functions can allow particles to interact with their environment in various ways.

Of course, not all particle functions exist for that purpose. Some of them focus only on the particle itself and do not care about the outside world. For example, ParticleAnimate() is a particle function that allows to animate a particle, so we do not have to put in the extra work. ParticleKill(), as another example, kills the particle prematurely, before its lifeCounter runs to 0. Particle functions do most of our work for us. They are like puzzle pieces, from which we build advanced behavior of particle effects.

Why do particle functions exist?

They are meant to do the “heavy lifting” of tasks performed with particles. Since they are compiled into the plugin DLL, they work more efficiently. Lua, as great and as fast as it is, will never outperform compiled code, such as that of a binary DLL file. For this reason, the more of the code we can delegate to the plugin, the better performance we can squeeze out of the particles!

On top of improving performance, particle functions simplify writing code for your custom particle effects. If you’re lazy, you will be thrilled to read this. If you’re ambitious, coding every kind of behavior might seem like fun challenge at first. Trust me that after a while, you’ll eventually become one of the lazy ones and would much rather have a function that already does it for you. The plugin luckily provides many such functions as part of the scripting API.

For the above reasons, it is always recommended to use a particle function if one exists for your intended goal, rather than attempt to code it from scratch. Coding your own particle functions in Lua should really be the last resort, if no suitable function exists for your tasks. In such case, you may ask the plugin’s developers if they can add your idea as a particle function directly to the plugin, especially if it is something generally useful. We’re open to such suggestions!

What do particle functions look like?

A particle function always begins with the prefix Particle, followed by some verb or a compact description of the task the particle will perform. For example, ParticleAnimate() animates a particle. ParticleLimitSpeed() limits the speed of a particle to a given maximum. ParticleCollidedWithItem() checks if a particle collided with an item.

I believe such a naming convention is quite clear, no need to push this topic further.

How to use particle functions?

Particle functions are just like other kinds of functions, meaning they take various arguments and may or may not return values. What distinguishes them is they always take a particle instance as the first argument, that being the particle which is instructed to peform the desired task (otherwise it wouldn’t be much of a particle function, would it?)
Everything else depends on what additional information must be provided to the function to perform its task and what is the result (return value) of the task. Particle functions are mainly used within group update functions, but they can also be called from init functions, which proves to be useful in some niche cases.

We will explore a few examples of particle functions and their usage in the next few sections.

Animated particles

A good particle function to examine would be the aforementioned ParticleAnimate() function. Naturally, this function will animate particles. It looks like this:
ParticleAnimate(part, start, end, frameRate) -- arguments of the ParticleAnimate() function
We can see that the function takes four arguments:
The function will cause part to cycle through sprite textures, beginning from the start index and ending on the end. The frameRate indicates how many frames it will take to move to the next texture in the sequence. The function instructs part to perform this “animate” task. What about the returned value? Well, the animation task does not require any feedback, does it? The particle is just supposed to move to the next “frame” of the animation sequence after the number of frames indicated by frameRate have elapsed. Hence, this function does not need to return any information.

You remember the code we used to get particles to loop through all textures in DEFAULT_SPRITES, back in the first section? Here it is again, in case you forgot:
local function dustUpdate(part)
    part.spriteIndex = (part.spriteIndex + 1) % 33
end
We can achieve the same thing by using ParticleAnimate(). Let’s try figuring out what arguments to use to get the same result.

First of all, we need a particle instance that we want to animate. Particle instances are passed to update functions. The alias for particle instances in the update function (by our convention) is part. Therfore, part is what we pass as the first argument to the ParticleAnimate() function (as we do for particle functions, generally speaking).

We want the particles to start with spriteIndex = 0 and end with spriteIndex = 32, after which they loop back to the start. What will the start and end values be? No, this is not a trick question, your gut instinct is correct. It’s 0 for start and 32 for end.

And what about frameRate? Well, in the original update function, the texture updated every frame. This meant that the frame rate of the animation was equal to 1. Which is the value we use for frameRate.

Putting it all together, the new update function should look like this:
local function dustUpdate(part)
    particleAnimate(part, 0, 32, 1) -- part, start = 0, end = 32, frameRate = 1
end
There, it is that simple. This exactly replicates the “modulo formula” from before. However, it is now done as a clean and simple call to a premade API function. No need to reinvent the wheel.
Notice if we want to change the animation range, we only adjust the start and end arguments passed to the function. You may be thinking, what happens if start is a higher index than end? Intuitively, the animations goes in reverse, from the higher index to the lower, no surprises here!

If we wanted the texture to update not on each frame, but only every n-th frame (e.g. every second frame, every third frame, fourth frame), it is only a matter of increasing the value for frameRate. With our “modulo formula”, would you be able to figure out how to make it so the animation updates only every 2nd frame, or every 3rd? It’s doable, but not so straightforward. ParticleAnimate() takes care of this business, you don’t need to distress over how its done.

Worth mentioning that there is a second feature to the frameRate argument. It accepts negative integer values, with a wholly different meaning. When you pass a negative value to this argument, it indicates the number of full animation cycles to complete in the course of the particle’s lifetime. The plugin figures out on its own how to adjust the frameRate of the animation sequence to fulfill this request. A frameRate of -1 tells the particle to complete one entire animation cycle. -2 tells it to complete two whole cycles, -3 indicates three whole cycles, and so on. This is useful to ensure that the particle will always finish on the end sprite texture, regardless of the particle’s lifeSpan.

Note for future reference: this function works identically for mesh particles, but we will be covering those in the subsequent chapter.

Simple colliding particles

Let’s see another example of a particle function, ParticleCollideWithWalls():
ParticleCollideWithWalls(part, rebound)
This function makes particles detect collisions with walls and bounce off of them, changing their direction accordingly. It takes two arguments: The part argument speaks for itself, but what about rebound? It controls how much rebound there will be when the particle collides with a wall, for example a rebound of 1.0 means that the particle maintains 100% of its initial velocity after bouncing off the wall, 0.75 would mean the particle retains 75% of the velocity upon each collision, and so on.

Does the function return anything, though? Yes, it returns a boolean value (true or false, remember?) telling us if a collision occured on a specific frame. This can be useful, for example to play a sound effect when the particle bounces off the wall, or perhaps to spawn ricochet particles at the point of impact. You can use it like so:
local impact = ParticleCollideWithWalls(part, 0.8) -- detect collision with walls and bounce off with 80% rebound
if impact then -- if collision occured
    -- do something here
end
We do not necessarily need to place the boolean in a variable. An alternative to the above is:
if ParticleCollideWithWalls(part, 0.8) then -- if collision occured
    -- do something here
end
but depending on your familiarity and coding preferences, such code may be harder to read.

We have a function for collisions with walls, what about floors and ceilings? There is a separate particle function for that – ParticleCollideWithFloors(). Although its name does not mention ceilings, it handles ceiling collisions as well. This function has more arguments than the wall function:
ParticleCollideWithFloors(part, rebound, margin, accurate)
The arguments for this function are: These additional arguments allow to fine-tune the behavior of the floor/ceiling collisions, depending on the use case. Regardless of these parameters, ParticleCollideWithFloors() also returns a boolean informing us if a collision with a floor or ceiling occured.

Let’s put these two functions to use and create some bouncy particles that will bounce around the room! To simulate some colorful bouncy balls, we will use the following init function that spawns colorful particles from Lara’s head mesh:
local function bounceInit()
    local part = createSpritePart(bounce)

    part.emitterIndex = getLaraIndex()
    part.emitterNode = 14
    part.lifeSpan = 180
    part.sizeStart = randint(50, 250)
    part.sizeEnd = part.sizeStart

    part.spriteIndex = 14
    part.fadeIn = 10
    part.fadeOut = 10

    -- we use colorHSV() to get a random color
    local hue = randfloat(0, 360)
    part.colStart = colorHSV(hue, 1.0, 1.0)
    part.colEnd = part.colStart

    local px = randfloat(-64, 64)
    local py = randfloat(-64, 64)
    local pz = randfloat(-64, 64)
    part.pos = vectorXYZ(px, py, pz)

    local vx = randfloat(-64, 64)
    local vy = randfloat(-128, -64)
    local vz = randfloat(-64, 64)
    part.vel = vectorXYZ(vx, vy, vz)

    -- finally, we set a positive Y accel for our "gravity"
    part.accel.y = 6
end
Okay, we have an init function, so it would seem we are ready to move on to the update function. But before we get to it, I have to go on another tangent, so bear with me…

Particle room index

If you remember way back in the introduction, we mentioned a room index as being one of the properties of a particle. However, we did not bring it up anywhere in the whole of Chapter 1. The reason is simply, if our particles will not be doing any collision detection, there is no real reason to set a room index to a particle. This room index is not used for any other purpose, so it is irrelevant in this case. The room index can just as well remain at the (default) index value of 0.

This matter changes if we have particles that actively test for collision (especially collision with room geometry) in the update function. Why is this suddenly so important?

Well, you see, the spatial coordinate system of the TR engine is more complex than it would seem at first. Normally, in an ordinary 3D world, it is enough to specify 3 cartesian coordinates, such as (X, Y, Z), to uniquely describe every possible location in the 3D space. However, the world of the TR engine is not so… ordinary.

In our beloved engine, there are cases where the same XYZ coordinates can occupy two completely different positions in the level map! Surprised? Allow me to jog your memory. If you played Tomb Raider III, do you remember the UFO at the end of the Area 51 Nevada level? Presumably due to some advanced alien technology, it was bigger on the inside than on the outside. How was this even possible all the way back in the 90’s? Well, the answer is that aside from 3D XYZ coordinates, there is also a fourth room “coordinate” that describes where Lara, the camera, or some other object is currently located. As seen with the UFO, rooms can overlap but still be considered two separate locations (this is observed when examining the layout of the Area 51 level map in the level editor). It is only with these four coordinates (XYZ + room index) that one can unambiguously describe an object’s location in the engine.

For most objects, during the rendering phase the engine performs room culling, meaning that objects belonging to a room to which no portal is currently visible, will not be rendered. This save computing power usually, since it is more efficient to first check room visibility before individually evaluating each object’s visibility for rendering.

Particle rendering done by the plugin does not perform room culling, though. Why is room status relevant to particles, then? Well, if a particle must test collision with room geometry (but also with moveables which are located in the rooms), you need to specify to the particle which room you are talking about. This is because, as was already pointed out, the XYZ coodinates alone are not enough to fully resolve the location. In the situation that two rooms (room A and room B) overlap with each other, not specifying the right room could mean that the particle is visible in room A, but seems to be colliding with the geometry of room B! What about situations where a particle crosses a portal to a new room? This is handled by the plugin, if the collision functions are used, the room index will be accordingly updated. But for this to work reliably, the initial room index of where the particle spawns should be accurate to where it actually is.

We now know why we should specify a room index for particles for the purposes of testing collision. This room index is accessed by the particle field, you guessed it, roomIndex. It holds any value from 0 to the total amount of rooms in the level (note that flipped rooms are treated as separate rooms under a unique index and the room indices in the editor are often not equivalent to the room indices in the compiled level).

How do you acquire the correct room index? Well, I assume that by now you are spawning particles using the emitterIndex and not using the absolute coordinates. In such a case, you can take the emitter item’s room index and assign it to the particle. You can get that item’s index with the GetItemRoom() function.

This function takes a single argument – the Tomb4 index of a moveable (remember about the GetTombIndex() function in case you want to convert a NGLE moveable index!) and returns the engine-side room index. This returned value is suitable to be assigned to part.roomIndex directly.

Since in the init function we have chosen to spawn the particles from Lara, we have used the handy function GetLaraIndex() and assigned the result to part.emitterItem. Since the value is identical, we can either take the value from part.emitterIndex:
part.roomIndex = GetItemRoom(part.emitterIndex) -- assign emitter moveable's room index
Or just call GetLaraIndex() a second time to obtain the index of Lara (it should be the same value either way):
part.roomIndex = GetItemRoom(GetLaraIndex()) -- assign Lara's room index
Having done this, we are ready to use collision functions in the particle update function.

Particle functions for room collision

Now, we need the update function to call the particle functions with the particle instance:
local function bounceUpdate(part)
    local rebound = 0.8 -- for added realism, want each bounce to retain 80% of the velocity
    local minBounce = 0.0
    local margin = part.sizeCust * 0.5 -- set margin to half the part size

    ParticleCollideWithWalls(part, rebound) -- we need to call the wall function first
    ParticleCollideWithFloors(part, rebound, minBounce, margin, true) -- and then the floor function
end
I leave creating the group that utilizes these functions to you. You’re mature enough to do it by yourself!

The init function is not that significant, apart from one crucial detail: we set accel.y. The value we set it to: 6 – happens to be the TR engine's constant for gravity acceleration (used for falling objects like flares, grenades, rolling balls, even Lara herself). We set the accel to simulate the pull of gravity on the particles. Remember, gravity is downwards acceleration, so the positive Y accel will make our particles “fall” to the floor. We have nothing notable left to say about the init function, so let’s carry on.

The update function is where it gets interesting. We see that we first created helper variables that hold some of the argument values for the particle functions we will use. Not necessary, but it makes the code easier to read. Especially curious is the line:
local margin = part.sizeCust * 0.5
Here, we are not overriding sizeCust as usual, but uniquely, reading from it. Recall that if we don’t overwrite sizeCust, it will hold the blended value between sizeStart and sizeEnd! We are using the 1/2 of sizeCust to determine the collision margin. This means the floor collision will respect the size (radius) of the particle. You can verify this claim by changing the sizeStart in init or making the particle change its size during its lifetime (hint: set sizeStart and sizeEnd to different values).
Otherwise, if we set the margin to 0, the particles will end up visibly cliping through the floor, which is more noticeable the bigger the particle is.

For the accurate argument of the ParticleCollideWithFloors() function, we used the literal true value directly when calling the function, meaning the helper variables are entirely optional.
Afterwards, we make the function calls: first to ParticleCollideWithWalls(), then ParticleCollideWithFloors().

Check out the in-game result of this update function! After launching our level, we see that we have many colorful marble-like particles spawning from around Lara’s head. They fall towards the ground and as they hit the floor, they bounce up gain! We have just implemented rudimentary physics for our particles and it took only calling two particle functions to accomplish!

This is what I meant with the particle functions performing arduous tasks for us. The functions take care of whatever logic there is behind detecting collision with room geometry and calculating the new velocity after the particle bounces off. We do not bother with intricate details like these, we can focus on the “big picture” of what we want the particles to do.


Homing and following particles

Many times already, I advertised particles behaving like missiles that target a specific moveable, adjusting their trajectory to reach the object. Or for something more organic, particles that behave like fishes or insects, following a moveable, including but not limited to Lara. You can implement such particles with (relative) ease. This is thanks to yet another set of particle functions, ParticleHoming() and ParticleFollow().

Here is the first of them, ParticleHoming():
ParticleHoming(part, moveableIndex, targetMesh, turnRate, speedUp, predict)
As you can see, quite more robust. Here is the list of arguments: As you see, the function requires moveableIndex, the tomb4 index of the moveable item. These tomb4 indices are not identical to the NGLE script indices, but if you remember us talking about spawning particles from emitters in chapter one, there is a function to convert the NGLE index to a Tomb4 index. It is getTombIndex(), of course.
The next argument, targetMesh, can either take an mesh index, or alternatively have -1 or nil in its place. If we specify a mesh, the target position is adjusted to be at the center of this mesh, otherwise, the moveables item pivot position is used instead. Bear in mind that with many enemies, this origin point is at the feet level (as with Lara, for that matter), so you may want to specify a targetMesh.
The turnRate is a bit tricky to understand at first. It is a percentage (rather, a decimal fraction) of the difference between the current facing of the particle and the ideal facing for the particle to be oriented head-first towards the moveable. This may not be clear, so I made a graph that illustrates it: With a higher turnRate, the particle will reach the ideal facing faster than with lower values, which will make the partile seem more sluggish. It is one of those values that needs to be trial-and-errored until you develop an intuition for it. Interestingly, the turnRate can be negative, but I’ll leave it to you to figure out what that does to the particle.
The speedUp describes the by how much to speed up or slow down the particle on every frame while it is homing onto the moveable. This may or may not fit your use case for the homing particle, so you can opt out of changes to the speed of the particle by passing 0 to this argument.
Finally, we have the predict boolean. When true, the particle gains somewhat of an “intelligence”, in the sense that when homing to a moveable position, it factors in the direction and speed of where the moveable is travelling. There is no difference in behavior if the moveable is motionless or moving slowly. If the moveable moves quickly, try setting predict to true and false and see what difference it will make, if any.

Here is an example of using the particleHoming() function on Lara herself, but you may of course choose a different moveable:
local function updateFunc(part)
    local index = getLaraIndex() -- get Lara's index
    particleHoming(part, index, 0, 0.05, 0.1, false) -- particle will target Lara
end
Once in game, you will notice the particles speeding up towards the Lara and possibly curving to reach her as she moves around. The only true way to get a feel for how the parameters affect the particle is to play around with them (increase one value, decrease the other value and see what happens). Obviously, the particles do not actually do anything once finally reaching Lara. But the particleHoming() only does what it says on the tin, it homes the particle to the desired moveable object. Is not responsible for anything else. To make the particles actually do something once they touch Lara, we must employ another particle function, ParticleCollidedWithItem(). This function tests for collision with a moveable by checking if it is within the bounding box of the specified moveable item. Here is what it looks like:
ParticleCollidedWithItem(part, moveableIndex, radius)
And the arguments : Just like the wall and floor collision function, this function also returns a boolean which tells if a particle is colliding with an item (true) or it is not (false). Note that colliding here means that the particle’s position vector is within the item’s bounding box.
Furthermore, the radius spherically expands the collision range for the particle. At 0, the exact position of the particle is tested against the bounding box. A positive value, like 128, means that a sphere of radius of 128 units with a center at the particle's pos vector is tested with the bounding box. A value in the negatives, however, is also interesting - it describes how much the particle must embed in the bounding box before a collision is detected. Note however, that very negative values can potentially cause collision to not be detected at all (imagine if the radius is set to -512, but the bounding box of the tested moveable is less than 256 units in thickness, the particle will have no shot at ever detecting a collision)! Thus, if using negative values for radius, keep them reasonable.

Testing against bounding box collision is quite fast and can be done repeatedly by thousands of particles per frame with ease. We can thus use the function to test if the particles made a collision with Lara:
local function updateFunc(part)
    local index = getLaraIndex() -- get Lara's index
    particleHoming(part, index, 0, 0.05, 0.1, false) -- particle will target Lara

    -- next we test if part collided with her
    local collided = ParticleCollidedWithItem(part, index, 0) -- placing result in var is optional
    if collided then -- did part collide?
        -- do something here
    end
end
Of course, we are missing a something to put inside the if condition. Hmm, it would be super cool if Lara lost health if the particle touches her, would it not? But can such a feat be achieved even, in nothing more than a measly particle script? Alas, it can.

For now, assume what you are about to see is arcane knowledge revealed only to the worthy. As you have made it this far into the tutorials, I consider you worthy of it, hence I am revealing it to you.
You can access Lara’s current amount of health by writing Lara.hitPoints anywhere in the script. Knowing this, how can we decrease her health by 1 hp? Very simple, by performing an assigment of the form:
local hp = Lara.hitPoints -- copy Lara's hitPoints to var
Lara.hitPoints = hp - 1 -- subtract 1 and assign it back to hitPoints
We can also write it in a single-line equivalent:
Lara.hitPoints = Lara.hitPoints - 1 -- subtract 1hp from Lara
Let’s see this in action by putting this line inside the if block checking for collision between Lara and a particle:
local function updateFunc(part)
    local index = getLaraIndex() -- get Lara's index
    particleHoming(part, index, 0, 0.05, 0.1, false) -- particle will target Lara

    -- next we test if part collided with her
    local collided = ParticleCollidedWithItem(part, index, 0) -- placing result in var is optional
    if collided then -- did part collide?
        Lara.hitPoints = Lara.hitPoints - 1 -- subtract 1hp from Lara
    end
end
Give it a run in game! The particles now have the ability to kill Lara! No longer just a harmless particle effect anymore, huh?

I know, I know, you are begging me to tell more about this magic code that modified Lara’s health directly in the Lua script. But this topic requires its own chapter to cover it sufficiently. There, I first lay the groundwork to introduce built-in globals, so just be patient, I promise we will get there!
The particles have perhaps become a bit TOO deadly now, though. This is because all of the particles in direct contact with Lara are each applying 1 hp of damage, over several frames. That means that if there are 20 particles touching Lara, she will bleed out health at a staggering rate of 20 hp per frame (for reference, fire makes her lose health at 7 hp per frame).

We can do something to reduce the damage dealt by the particles. What if the particles disappeared when hitting Lara? Meaning that the particles would deal damage of 1 hp a single time and die immediately afterwards. We know particles die the moment their lifeCounter goes to 0. So we can directly set part.lifeCounter = 0 in the update function, right after doing damage to Lara, correct? Well, this is sound reasoning. But I do not recommend tampering with the lifeCounter and lifeSpan values outside of the init function. There are technical reasons for this that I prefer not to get into right now. Instead, the optimal solution is to call a particle function that kills the particle. I’ve mentioned it before, it’s none other than ParticleKill(). It takes just one argument, part. It immediately destroys the particle instance, but does it in a manner that is considered “safe” to the plugin internals. Just take my word for it for now – when you need to kill a particle, don't set part.lifeCounter to 0, do it with ParticleKill() instead.

Okay, so lets add the call to ParticleKill() after subtracting a hitpoint away from Lara:
local function updateFunc(part)
    local index = getLaraIndex() -- get Lara's index
    particleHoming(part, index, 0, 0.05, 0.1, false) -- particle will target Lara

    -- next we test if part collided with her
    local collided = ParticleCollidedWithItem(part, index, 0) -- placing result in var is optional
    if collided then -- did part collide?
        Lara.hitPoints = Lara.hitPoints - 1 -- subtract 1hp from Lara
        ParticleKill(part) -- and kill the part
    end
end
The above update function started out simple if you remember, but already grew into something quite substantial - we have a script which makes particles lock onto Lara, deal 1 hitpoint of damage to her and disappear. To accomplish this, we used a total of 3 different particle functions: ParticleHoming(), ParticleCollidedWithItem() and ParticleKill(). This is how we can create complex behavior of particles, by assembling various particle functions together in Lua code. The above update function can be a basis for other cases where particles must deal damage Lara. Note it down somewhere, it’s a solid template!

I think we’ve had enough examples of particle functions for now. I will not go through every single particle function and show what does, there are far too many and I would be wasting both my time and yours. If you want to learn what other particle functions there are and what they do, please check out the scripting reference section that deals with particle functions. In the tutorials, I would rather focus my energy on demonstrating practical examples that show how these functions can be used, all the while exploring other topics, many of which still remain. Let’s therefore carry on to the next topic at hand!

Particles that spawn other particles

I know you’ve been eagerly awaiting this one. Yes, update functions have the power of making particles spawn other particles! There are numerous use cases for this. Maybe we want a particle to leave a flame or smoke trail? Maybe the particle will spawn ricochet sparks when it hits a wall? A homing missile might cause an explosion when it hits its target? The possibilities are endless.

HOWEVER!!! This feature is a double-edged sword. You see, the ability of a particle to spawn another particle can potentially cause particles to spawn at an uncontrollable rate.

What can lead to this scenario? Well, imagine that you have a particle. In its update function, it spawns 10 new particles. 10 particles does not sound like much. But remember, update functions are executed on every frame. What if the parent particle (which spawns particles) has a long lifetime, like 10 seconds? 10 seconds is 300 frames. That means the update function for just that one particle is executed 300 times. Assuming the birthed particles live just as long as the parent particle, that lone parent particle will birth 300 particles. Okay, that still sounds reasonable. But what if there were several parent particles? Say, 20. Since each parent particle spawns 300 particles over the course of its life, 20 parent particles will spawn 20 * 300 = 6000 particles! We’re already into the thousands!

Nevertheless, this is still the more “optimistic” scenario, as we made the assumption that the birthed particles will not be bringing more particles with them. What happens if the birthed particles end up spawning particles themselves? I’ll tell you:

ALL HELL BREAKS LOOSE!!!

Let’s go back to one parent particle with a lifetime of 10 seconds. It will now spawn just a single child particle per frame. On the first frame, it spawns a child. We now have 2 particles. On the second frame, the parent particle spawns another child. But the (former) child particle spawns its own child particle. Now there’s 4 particles. On the 3rd frame, the 4 particles will each spawn their own child, giving 8 particles. You see where we are heading, right? On the 5th frame, we end up with 16 particles, on the 6th – 32, 7th – 64, so on each frame the number of particles doubles. This type of situation, where something doubles (or triples, or quadruples) in each successive iteration, is known as exponential growth. It is also known as the wheat and chessboard problem.
Take a guess, assuming the pattern continues, how many particles do you think we will end up with after just 1 second (30 frames) elapsed?

1 073 741 824 (that’s right, over 1 billion!)

Are you shocked? Try doing the math yourself, if you don’t believe this is the case!

I will inform you right away – the particle system plugin cannot deal with that many particles by a long shot. You can expect to achieve somewhere around at most 8000 active particles with this plugin, due to the technical limitations of the engine. The peak of what modern, state-of-the-art game engines can reasonably achieve these days is around 50-100 million particles, if a high-end GPU is used. That’s considerably more than our plugin, but let’s put things into perspective: the TRLE engine is almost 30 year old technology at this point. It cannot utilize our fancy, expensive GPU cards. It would have to be rewritten from the ground up to do so.

Back then, when GPU technology was less advanced, most of the work had to be done by the CPU, excluding very specialized tasks that only GPUs could do. This is why the cap on particles with our plugin is so low. All the more a reason to be very careful when spawning particles from other particles when using this plugin.

When spawning particles in update functions, you must employ strategies to restrict the growth of particles. You do this by not getting too carried away with the number of new particles you are spawning, as well as keeping the lifetime of the birthing (and birthed) particles within reason. And most certainly, avoid situations when particles spawn new particles of the same group, as that is precisely what will lead to exponential growth scenarios.

You must have noticed the particle counter visible on the screen. It tells you how many particles are active at a given moment. If you catch notice that the number of particles keeps growning and never stops, you know you have a problem somewhere with the number of particles being spawned. Be mindful of this counter when you are creating your effects, especially when dealing with particles-spawning-particles.

Now, with all of the warnings and cautions out of the way, I’ll stop being a stick-in-the-mud. Let’s have a bit of fun!

Where and when can particles be spawned

Do you remember what function is responsible for spawning new particles? You might reply that it’s the init function. That’s correct, but which API function does init use to achieve that? It calls createSpritePart(), remember? Let’s take a closer look at it.

What arguments does createSpritePart() expect and what does it return? Simple, it expects a particle group as the first (and only) argument. As a result, it returns an uninitialized particle instance, fresh from the press. That’s what I presented in the first chapter of the tutorial and we have sticked with it, since. But we’ve gotten more experience with this plugin since then, how about we poke around a bit with these rules?

First of all, where can we actually make a call to the createSpritePart() function? Can we just call it anywhere? Let’s create a setup for a particle effect test, as below:
local test

local function testInit()
end

local function testUpdate(part)
end

test = createGroup(testInit, testUpdate)
This creates a particle group, but one which does not yet cause particles of its “type” to spawn anywhere, since there is no createSpritePart() in sight. Let’s try calling it somewhere, but we’ll deliberately not go by the book and place it outside of the init function, to create a particle instance:
local test

local part = createSpritePart(test) -- calling createSpritePart() outside of init

local function testInit()
end

local function testUpdate(part)
end

test = createGroup(testInit, testUpdate)
After launching the game, we are greeted with an error in the console, like the one below:
DATA\tut1.lua:3: calling function "createSpritePart" is forbidden in this phase
The error message is a bit cryptic, but it signals that the plugin did not like the function being called in the outer script. The error says that calling the function is “forbidden in this phase”. What’s all that about?

Script phases

I have not yet dived into this topic, but I suppose now is a good opportunity. The plugin distinguishes three phases when it reads our Lua scripts: The first phase is the level-loading phase. This phase happens when a new level has just been loaded and just before entering the main game cycle, where we control Lara or cutscenes and flybies play out. If a level script file exists, it is loaded at this moment.

Once the script file loaded, all the init and update functions are defined and particle groups are created (via createGroup()). Speaking more generally, all Lua code that is outside of the bodies of init and update functions gets performed by the plugin in this loading phase. There are no particles present in the level yet, however. Which would make sense if they are spawned in init functions all the time. The level-loading phase occurs only once and never again, until a new level or a savegame is loaded.

The other two phases are the init phase and update phase. They occur once the game is running the level, on every frame. It is not hard to guess what these phases are meant to do.

During the init phase, the plugin goes through the particle groups defined in the level and executes their respective init functions (though not all groups will do this, as we will learn later). Whatever code is in the init function will get performed during this phase.

During the update phase, the plugin further iterates through all currently existing functions in the level and executes the respective update function on behalf of that particle. Whatever code is in the update function will get performed as many times as there are active particles of a given group.

You can see that this system is a bit complex, because it separates what happens outside of init / update functions and what happens inside those functions. There are good reasons for this separation, however they are technical and I don’t want to bore you with the details.

Creating particles in update functions

Let’s go back to the snippet from just a minute ago. We are deliberately calling createSpritePart() outside of an init function. This outer scope corresponds to the level-loading phase. The error message tells us calling this function was forbidden in this phase. What the plugin is telling us is that it does not want particles created during the level-loading phase, period.

Okay, so we’ve established so far that the plugin allows creating particles in the init phase (in some init function), not during the level-loading phase (in the outer portions of the script). What about the update phase?

Well, it doesn’t hurt to try, but remember the warning I gave about spawning the same particle type in an update function?

For this reason, we need to make a separate particle group. This way, the parent particles and child particles will have different update functions, which prevents an exponential growth scenario.