LuaSTG Pattern Making Tool-Tasks

From LuaSTG Wiki
Jump to navigation Jump to search

We've talked about how for loops can be parametrized into functions allowing us to spawn several bullets with a single function call. There is another issue that we've been ignoring all the way till now. Recall that there are five assumptions made on the function SpawnBullet, the first one says a call to the function spawns a bullet immediately with no delay. Later in the for loop examples there is no waiting in the loops we've written, meaning after the program enters the loop it will immediately spawn all the bullets and finish execution.


Patterns in actual shmup games have delays. With delays, we can specify a single bullet to be shot not immediately, but rather n frames after SpawnBullet is called, where n is an arbitrary non-negative integer number. For the loops that create many bullets, we can specify a time interval between the spawning of two consecutive bullets. Such delays come handy for patterns where waves of bullet come one after another (down to 1 to a few frames for small waves, and up to one or several seconds for larger waves). We could not have delays with the for loops we defined, for reasons described below.


Why do We Need Tasks[edit | edit source]

Many games' implementations define two functions:

  1. The first function is a update function; each time this function is called, the game will update the inner representation of the game and by one time step. Conceptually each call to update advances the time axis by one time step. Here the word "time step" is interchangeable with the word "frame". There is no time elapsed between the start of the update function and the end of the update function. Only after the execution of update function, the time advances.
  2. The second function is a render function that draws the game content onto the screen.


The game you are working with is probably not an exception to this model. Our parametrized for loop generates bullets and therefore changes the states of the game (as bullets are part of the game state), so it should be put inside the update function. However, both the beginning and the end of the loop are wrapped inside the update function, and the for loop can not wait any time at all unless it delays everything else that is happening in the update function, which is not a good thing.


A solution to this problem is to allow the for loop function to be paused and resumed. When the program executes update function and reaches the for loop, we call a wait function inside the for loop. The wait function stops the execution of the for loop and transfers the program execution back to finish up updating the rest of the things in the game. In the next execution of the update function, it will resume the for loop function from the position immediately after the line it was paused. After being resumed, the for loop can decide to either pause again or to finish executing the rest of the function. This allows the total execution of the for loop to spam across several time steps in the game, while the for loop itself still being inside the update function.


The pausing and resuming functionalities are implemented by tasks, also called coroutines or async. We can pass a function as parameter to a task, and let the task handle the execution of the function. Using tasks, we can pause a piece of code by the following procedures:

  1. add a wait function after the piece of code you want to pause
  2. put the piece of code inside a function f
  3. create a task, pass the newly defined function f to the task (as one parameter)
  4. put the task inside update function, so it is executed every frame


Now the task will start function f until it executes to the first wait function call and pauses. Then when we resume the task, f resumes; when we pause the task, f pauses. (and of course f executes the piece of code we originally want to pause, so pausing f is equivalent to pausing that piece of code)


To be clear, although I'm using the game loop as an example to explain why for loops can not have wait without using tasks, it is only for explanation purposes. The problem arises from the sequential execution of programs. After all, the for loop must finish before the program can update the rest of the game.


Task Creation and Destruction[edit | edit source]

Not all tasks adhere to the rules below, but for the purpose of this tutorial I will assume the following:

  1. After a task is created, it will be resumed once and exactly once each frame after, until 1) it is destroyed OR 2) it finishes execution
  2. As a more strict requirement to the previous rule, assume everything else in the game is updated exactly once between each time the task is resumed; with the exception of possibly other tasks' execution
  3. Every task is managed by a master, which is a game object or table; the master will destroy the task when itself is destroyed; if a function's task is destroyed before it finishes execution, then the rest of the code of the function will not be executed


The creation and destruction of a task is associated with a master, because we don't always want a task to end its own execution when the last line of its code is reached. Consider the situation when an enemy uses a task to spawn bullets. If the bullets are still being spawned around the enemy after it is deleted, it would not make much sense. The association between a task and a master is for the need of a way to destroy the task when the master is deleted. The solution is to put tasks under the master and let it resume the task each frame when it is alive, and let it destroy the task once itself is deleted.


LuaSTG users may take a look at the page LuaSTG Tasks to see how tasks can be created.

For Loops with Tasks[edit | edit source]

Let's see how we can let the function execution be temporarily paused before and inside a loop. We will be modifying the SpawnSpiral function from last tutorial.

Using Task in a For Loop


Here the function is largely unchanged, but a few things have been modified.

  1. the function accepts three new parameters, a task holder, a initial waiting time and a loop wait time.
  2. the function will create a task at start to hold all the code inside itself (an actual implementation will put create task outside a function call to SpawnSpiral).
  3. it wait t frames before spawning the first bullet, and then generate a new one every dt frames.


The wait function is a loop that pauses once, and then pauses again every time immediately when it is resumed. In total such a loop will pause n times. You may be wondering why t and dt can be floating point values instead of integers, as there is no such a thing as to pause half a frame. That is because sometimes we would want to make a bullet appear as if it is spawned at some float time value. This is a feature that I've integrated into the tool I've written, though I do not think it is an important feature for the purpose of making patterns, and there are some potential pitfalls (see exercises in the end of first part of tutorial). For those that do not need it, you can assume input t and dt will be integers.


With the above assumptions we made we see that tasks enables a loop to be paused while the other parts of the game are updated. We can create many such loops by calling the above function many times. The tasks created in this case appear to execute concurrently; but actually they are executed in order, and are just resumed, executed and paused once per frame.

Nested Tasks[edit | edit source]

The function that a task can hold is not much different from a task-less function except that it can be paused, and it should be no wonder that a function hold by a task can create another task. Think about the example below. (image is captured after the enemy spawns the last wave of bullets)

A Nested For Loop with a Task Created in each Loop; Note the Second Wait 10 Frames Belongs to the First Task


After the inner task has been created by the function hold by the outer task, both tasks will execute concurrently (though the inner task appears to be nested inside the outer task); in fact, there may be multiple tasks created by the inner task executing at the same time, but in this example only one inner task can be alive at any moment. For this example, We can calculate and predict the time interval between two waves of bullets (each wave refers to bullets shot by one outer task resume). The time span from the start of a wave to the start of the next wave is defined by the waiting time in the outer task, which is 10 frames. The first bullet of the wave shot 4 frames earlier than the final (5th) bullet of the wave, and we know the time interval between the first bullet in the next wave and the final bullet in the current wave would be 6 frames.


Two Delays For Each Loop[edit | edit source]

We can parametrize the waiting time in a for loop that is held by a task much like we've parametrized increments. There are a few differences, however, one being that the time is not a variable in a loop. In other words, we do not have a timer variable for the loop. The wait time specifies a time difference (delay) instead of a time point. This means we do not need to have an initial time value, unlike other incrementable values. It is perhaps not obvious that we can store two delay variables for a given for loop. One for the delay before the program reaches and starts executing the for loop, one for the waiting between consecutive executions of the for loop. We will call the delay in the first case a total delay denoted with the variable t, and the second case a repeat delay denoted with the variable dt.


It may not appear that the variable t is needed. It turns out that sometimes it is useful to have a piece of code about a specific bullet, and have that piece of code executed a certain amount of time before the bullet is even spawned. Suppose lots of rings of bullets are spawned instantly, each ring is spawned at a uniformly random position inside a rectangular area. Suppose there is a point P, and you want to add a flashy spawn effect that the bullets closer to P are generated first, and then bullets further away from it are generated later. In this case, the spawn position has to be decided by a using random number generator before deciding the delay of spawning the bullet.


This effect would be hard to implement using a nested for loop and SpawnBullet with the only waiting being the repeat delays, because the delay for each bullet is independent from each other and randomized (as it depends on the randomized spawn position), so that the time differences between spawning two bullets are not constant across all of them. Consider the following code that overcomes this issue with the use of total delay.

Generating bullets out of sync with each other


Note here the outer loop is not doing any waiting. Instead, it spawns lots of tasks and each of them will do a bit of waiting, after which will spawn a ring of bullets. The execution of the inner for loop is delayed by the distance between the spawn point P and the boss.


This example can be fully parametrized to a function like other examples. However, one thing we are not yet equipped to deal with is the piece of code that generates a random position inside a rectangle. It does not fit our model well, in that we are missing a way to specify which randomization procedure to take without rewriting the whole loop. What if we are going to spawn them randomly inside a disk? how about on the circle? on a polygon? There are infinite variations, and their implementations get tedious if we can not get them done quickly. In later tutorials I will talk about a systematic way to implement different types of value randomizations by inserting pieces of code before the execution of inner for loops. For now we are going to ignore the randomization procedure.

Parametrizing the Waiting[edit | edit source]

Finally we are ready to get down to the dirty job. A typical nested for loop with two types of delays described above can be parametrized into the following:

A somewhat general parametrization of a nested for loop


The difference between this example and the parametrized nested for loop from the previous tutorial is the addition of tasks and extra delay variables. t1 specifies a total delay for the pattern. dt1 specifies a delay for each execution of the inner for loop task. t2 and dt2 specify total delay and repeat delay for the inner for loop task.


Notice with the addition of tasks and delay variables, it does not change the fact that we need to duplicate every incrementable variables x, y, a, v so the increments in the inner for loop will not have global effect on the variables incremented in the outer for loop.


When we count the parameters, 5 new parameters are added to the list, they are:

- a reference to master (which is the boss in the previous examples)

- 4 delay variables, 2 for each for loop


It may be clear to you by now that this nesting of for loops can go deeper, and for each for loop nested, we can just add:

- 1 variable for number of times the loop repeats, and

- 4 new incremental variables

- 2 delay variables


It would be nice if we can separate those variables into groups and modify them separately. One schema is to group all the initial variables, the reference to master, the non-incrementable variables together as the initial parameters, and group the number of times loop repeats, increment variables and delay variables by the for loop they belong to. For this we would have n + 1 groups of variables for an n-nested for loop.


I will use a modified schema for grouping. The first group for initial parameters will be further divided to incrementable variables and other variables. The incrementable variables will be defined outside the for loops and is used and incremented within the for loops. The other variables can be thrown inside the innermost for loop before calling SpawnBullet, since they never get changed (at least in the case of the above naive parametrization). An exception that doesn't belong to either group is the master variable, it needs to be accessible to all layers of for loops in order to create tasks for them to execute. However it is also not an incrementable variable like others defined outside the loops. In some cases the master variable may even need to be different for each layer of for loop (and this is something that I have not spent time looking for a satisfying solution).


The details of how the grouping is done will be discussed in the next tutorial. The basic idea is that we can use a table to hold each group of variables, and the accesses and modifications on local variables become accesses and modifications on table variables. Additionally the copying step before the inner for loop in the above example can be simplified into a simple copy of the whole table.