LuaSTG Pattern Making Tool-Value Propagation

From LuaSTG Wiki
Jump to navigation Jump to search

First Matrix[edit | edit source]

The first problem we will solve is about finding an efficient way to feed all the initial values of incrementable variables and their increments in the for loops into the columns. Intuitively for an n-nested for loop and an output column, if we have m incrementable spawn parameters, then there will be one set of m initial incrementable variables' values and n sets of m increments that need to be assigned. To fill in these n + 1 sets of values, we could use a matrix of size m * (n + 1) like the following (n = 1, m = 4):

Fields Initial Values Increments
x 0 0
y 0 0
a -90 9
v 1 0.1

When we need to spawn the pattern, we can define one output column and one parameter column, and use the matrix above to feed the values into the parameter column automatically (the output column needs neither initial values nor increments, so it does not have anything to do with the matrix content). This is a relatively faster way to input the initial values and increments, when compared to directly assigning them "col.x = 0; col.incX = 0; ..."

Loop-Related Variables[edit | edit source]

How about other loop related variables like s_n, s_t, s_dt or even the non-numerical variable s_script? We can add them to the matrix as well by leaving blank the initial values column (since those values don't have an initial value). The results are shown below:

Fields Initial Values For Loop #1
s_script - empty
s_n - 10
s_t - 0
s_dt - 0
x 0 0
y 0 0
a -90 9
v 1 0.1

This table is actually describing the exact same set of parameters we have used in an example of spawning spiral-shaped bullets in the previous tutorial Parametrization.

Zeros by Default[edit | edit source]

Notice many cells of the previous matrix are 0 or empty (empty in s_script refers to no scripts used in the column), for the ease of our eyes we can just leave them blank and set the values to 0 or empty by default. The results are as follows:

Fields Initial Values For Loop #1
s_script -
s_n - 10
s_t -
s_dt -
x
y
a -90 9
v 1 0.1

We can interpret the values in the above table as follows. The pattern will spawn 10 bullets at position (0, 0) with no delays, each with angle starting from -90 incrementing 9 every loop, with speed starting from 1 incrementing 0.1 every loop.

Nesting and the Re-ordering Property[edit | edit source]

A loop nesting can be created by adding a new column and fill in the respective increments. One thing interesting about nesting for increment loops is that re-ordering the columns will not lead to any change in the pattern. Two equivalent versions of the same pattern are given below.

Version1:

Fields Initial Values For Loop #1 For Loop #2
s_script -
s_n - 6 24
s_t -
s_dt -
x
y
a 360 / 24
v 1 0.1

Version2:

Fields Initial Values For Loop #1 For Loop #2
s_script -
s_n - 24 6
s_t -
s_dt -
x
y
a 360 / 24
v 1 0.1

The equivalence can be proven with vectors. Suppose vi = <xi, yi, ai, vi>, v1 = <x1, y1, a1, v1>, v2 = <x2, y2, a2, v2> contain the initial values and increment values respectively, then there will be 24 * 6 bullets shot in total. Each bullet is spawn with a vector of spawn parameter vp = vi + v1 * j + v2 * k, where j is an integer in the range [0, 23], k is an integer in the range [0, 5]. Since the order of addition does not matter for vectors, vp = vi + v2 * k + v1 * j. Although we have v1 and v2 exchanged position in the expression, it still generates the same set of bullets.


This nesting re-ordering property can also be proven for non-zero delays:

  1. for a matrix with columns that have non-zero repeat delay s_dt, those columns can be re-ordered without affecting the results
  2. for a matrix with columns that have non-zero total delay s_t, the individual s_t values doesn't matter as long as s_t of all columns sum to the same number (and yes this also means they can be re-ordered without affecting the results)
  3. however these properties are broken (in a predictable way) when column scripts are added into the equation


It is rare for these matrix to have more than 4 to 5 columns. Recall these columns are just another way to write nested loops, and it is uncommon for the code to have 5 layers of for loop nesting. The fact that number of bullets grow exponentially as the number of loops gets larger is also a limiting factor of the number of columns, especially when the number of repetition is large in each column.


Linking[edit | edit source]

The matrix contains all the information we need to generate a sequence of parameter columns, except the following ones:

  1. we have not specified anything about the variables of the output column (non-incrementable variables etc.)
  2. and we need to set s_next of the final parameter column to that output column
  3. s_master is not provided


Taking the above points into account, we want the output column to be provided to the algorithm as a parameter beside the matrix.

We also want the variable s_master of all columns to be shared and specified to the algorithm, so one matrix can be re-used among different kinds of enemies or bosses. The pseudo-code for the algorithm to spawn bullets is here:

A function that links the matrix with an output column


This function links the parameter columns generated from the matrix with the output column, assign master to every column and call spark function on the first column. An actual implementation would break this function further down to three smaller ones:

  1. Link(matrix, outputColumn) generates parameter columns from the matrix and link the final column with the output column, return all columns including the output column
  2. SetMaster(columns, master) sets all columns' master to the given one
  3. SparkFirst(columns) call spark function on the first column in the array to initiate the generation of the whole pattern


We can define an output column that copy its own parameters to the first column of another matrix and call SetMaster followed by SparkFirst on it. This allows matrices to trigger other matrices to generate patterns through this output column. This feature is implemented in the tool.

Spawn Parameter Selection[edit | edit source]

As you may have realized, there is something wrong with the above set of incrementable variable. We defined x, y, a, v to be incrementable but all the way till now the variables x and y are never given any increment in the loops (except in exercises). We can remove those values from the matrix in most cases because they are not frequently used. A question is what set of incrementable variables would be best instead of x, y, a and v? The answer depends on your use cases, that is, the spawn parameters of the actual pattern you are going to generate. Be aware that when it comes to 2-d coordinates, there is always the choice of either using polar coordinates or xy (also called standard) coordinates. I would recommend experimenting with the following set of incrementable variables if you are aiming for Touhou-style patterns.

Variable Description
ra angle from boss to the spawn point
r distance from boss to the spawn point
a offset angle of the movement angle (see below)
v speed of the bullet in unit distance per frame

We will rewrite the spark function of an output column to transform ra, r, a, v into the x, y, a, v variables and then spawn the bullets. The transformation is defined as

x' = s_master.x + r * cos(ra)
y' = s_master.y + r * sin(ra)
a' = ra + a OR a' = a
v' = v

The bullets are spawned at (x', y') with speed v' towards angle a'. There is a choice that a' may be ra + a or just a itself, and this will be discussed in the next tutorial. However, in many cases the above simple set of parameters would not suffice, and you would need to add new ones to or remove some from the above list. This can be done with the below procedure:

  1. Study the interface that is offered, write down the set of spawn parameters
  2. Select a suitable way to represent or transform those parameters. In the above a polar transformation is applied to the variables x and y so they becomes ra and r. That is, we choose to use the polar coordiantes ra and r instead of the standard coordinates x and y.
  3. Add the result variables as rows of the matrix
  4. Modify the output column, or column scripts in the final column of the matrix to interpret those variables; this will be discussed in the next tutorial

Limitations[edit | edit source]

Before moving on, we will talk about some limitations of this algorithm that we have not yet discussed how to overcome.

  1. randomization of bullet speed or angle can not be implemented
  2. it is unclear how aimed bullets of various types can be implemented. E.g. Aiming from bullet position to player position or aiming from boss position to player position.
  3. a small change like spawning two bullets instead of one on each shot with slightly different parameters may require duplicating the entire matrix
  4. it is unclear how less common pattern shapes like polygons or ellipse can be implemented without making a hell lot of changes to the output column (which you probably want to avoid for it is time consuming)
  5. similar to the previous point output column needs to be changed if you want to add another polar offset ra2 and r2 on top of the original ra and r (so the two offsets add up to the final spawn position x' and y')
  6. the repeat delay between each execution of the loop is fixed
  7. loop execution that depends on the results of the previous execution can not be implemented. E.g. A loop that keeps adding random number to the spawn position
  8. loops that repeat variable number of times can not be implemented.
  9. simple arithmetic such as multiply and assign the results to variables are hard to do
  10. rotating of the whole pattern by an angle can hardly be done if we need to


Column scripts offer solutions to the above problems. There are a few other problems that you may encounter that do not need a column script to solve. They are briefly described below.

  1. if you want to customize the bullet movement path, E.g. things like acceleration over a period of time or rebound after touching one side of screen, you can first write an interface that spawns such a bullet given the parameters like initial speed, position of the boundary and then define an output column that uses those parameters to spawn the bullet.
  2. About non-incrementable parameters of a bullet, usually they do not need to be changed at all mid-generation. In case one such non-incrementable parameter needs to be changed based on the current loop repetition count, you can set an incrementable variable that increments in that loop and use an if statement to test that variable in the spark function of the output column. This can also be done in various simpler ways with column scripts.
  3. In the previous point, the spark function of an output column can be overridden to add that extra piece of code.


There is also one problem that unfortunately I have no solutions to offer. The performance of this algorithm is poor in practice, probably due to large number of function & table & task creation and destructions as well as complex value copying and many function calls. On my pc the FPS drops above 2000 bullets, and if many bullets (1000+) are generated at the same frame there will be a visible lag on spawn.