LuaSTG Pattern Making Tool-Column Scripts

From LuaSTG Wiki
Jump to navigation Jump to search

In this tutorial we will go through some examples of column scripts. Column scripts are short pieces of code (functions) that are executed between two columns: the current column cur and the next column next. The current column must be a parameter column, while the next column can be either a parameter column or an output column. A restriction for a function to be a column script is that it needs to takes in three parameters "cur, next, i".


Column scripts can be assigned to the s_script field of a parameter column. After doing so, calling the spark function will trigger the script to run cur.s_n time, each time before running the inner loop represented by next. As the most basic form of column objects have very limited uses, and their flexibility are far from what we can do with writing simple loops, column scripts are essential to the pattern generation because they provide the same flexibility we want from simple loops.


Defining Column Scripts in the Code[edit | edit source]

A Column Script is simply a Function with Parameters cur, next, and i

To define and use a column script, you need to define a function and pass its reference (or for some languages, its pointer) to the s_script field of a column object, and that column object is the one referenced by the parameter cur. Here is a table explaining the purpose of three parameters of a column script.

Fields Description
cur (column object) the column object representing the outer loop. It will usually hold states that are enough to generate all sub-patterns.
next (column object) the column object representing the inner loop. It will usually hold states that one sub-pattern uses.
i (number) the number of times this loop has been repeated. This is an integer counting upwards from 0.

Typically the goal of a column script is to modify the states (by states, I mean the spawn parameters stored in the column, as well as other parameters in there like s_t) of next column. The modification on the states will propagate over to the end of a matrix, and change the spawning parameters of the objects spawned as we intended. For example, we may want to randomize the bullet shot angle by adding a script to randomize next.a in one of the columns.


Occasionally we may also want to modify the states of the column cur. Recall that cur represents the relative outer loop and next represents the relative inner loop. Changing variables on cur will influence the next repetitions of the outer loop, while changing variables on next will only have effect on the execution of the inner loop in the current repetition of the outer loop.


The variable i is for tracking the number of times the loop has repeated, listed as the third parameter because it does not depend on either cur or next. i starts at 0 and ends at cur.s_n - 1.


There are a lot of tasks that can be implemented rather nicely in column scripts. It may be the quickest to show some examples of actual uses of column scripts in order to demonstrate what they are.

Examples[edit | edit source]

Random[edit | edit source]

Our first examples are about implementing different kinds of randomization as column scripts. Here we consider three different scenarios that we may want to add a randomization procedure that randomizes some variable(s) in each loop repetition. In the first scenario, we want the value of a parameter to be a different value in each repetition; we don't want to give up the original value calculated by the increments by assigning the value to, say, next.x, which will overwrite the original value of x. Instead we want to have the randomization as a random offset of the original value, and we can set the original value to 0 as well if we just want a simple randomization.


For that, we will introduce the first idea for writing column scripts, which is preferring variable increments over assignments. The logic is that assignments are less general since an assignment is just a special case of addition where the base value is 0. Consider the following column scripts.

Randomization scripts


The first function R1 gives a solution to the first scenario by putting a random offset on the variable next.v, randomizing it in the range of next.v - 1 to next.v + 1. Notice instead of writing next.v = Random(-1, 1) we choose to write next.v += Random(-1, 1), there are two advantages to write it this way. One being that we may want the randomization be an offset that would take into consideration the original value of v. Another being that there can be multiple columns scripts in a matrix acting on a single spawn parameters in different ways, and using assignment would mean that only one of them will take effect (being the one that writes to the value the last time); we can use addition to combine the effects of all those column scripts, so the resulting parameter as a sum of all values added by those scripts will reflect the value change in each of those scripts.


In the second scenario, sometimes there is need to vary two or more parameters randomly in a related way. For example, if we take a random point inside a circle, the x and y position of the random point would not be independent (from a statistical point of view) from each other and so we want to randomize them together instead of separately. The solution is pretty simple and is shown in the function R2. We use a single column script instead of two independent scripts to handle this randomization.


In the last scenario, we consider a rather special case that the randomization has a lasting effect on the following execution of the loop (This is (perhaps quite obviously) inspired by Meiling's mid-stage non-spell from EoSD stage 3, where the spawn point of rings of bullets appear to be moving randomly each time a ring spawns). The trick to implement this is rather simple. Instead of varying the value of parameter under the next column randomly, we vary the value of it under the current column, as illustrated above by R3.

Coordinates[edit | edit source]

In the second example, we consider a very common case where we need to convert between standard and polar coordinate systems. This may occur the most often when you use polar coordinates when computing the spawn x, y. Assume the origin of polar coordinates is the same as origin of the standard coordinates (otherwise we use a script to translate it, see the FOLLOW script in rotation and translation section below for example), we can use this script to convert from polar coordinates to standard coordinates.

Coordinate conversion scripts


As shown in the image, four parameters are needed for the conversion: two for input (ra and r) and two for output (x and y). It becomes a bit more complicated when more polar transformations are applied to other variables at the same time, as we need to use more scripts to represent them, this will be discussed later.

Player Position[edit | edit source]

The use of player position in the generation of patterns is common. There are lots of ways to use the coordinates of a single point (player.x, player.y) in changing spawn parameters. One is to make an "aimed" pattern by calculating the angle from the bullet spawn point to player position and shoot a bullet in that direction. In other words, the bullet will be shot towards the player. Another is to spawn the pattern around the player instead of the enemy by adding the player position as an offset. The first case can be done with the following script.

Aimed angles


Again here we use increment in place of assignment. When the shot angle a is originally 0, the result angle will be aimed at the player; if it is non-zero, then its direction will be off to one side. As most aimed patterns involve bullets not directly aimed at the player, this modification generalizes to a much larger set of patterns.


Variable Delay[edit | edit source]

Back to the function that execute the loop body, we made so that before the task waits -self.s_t frames before the start of the loop, and self.s_dt frames between each repeat of the loop. This allows us to change the spawn delay of the next column by setting next.s_t, and the loop repeat delay of the next column by setting next.s_dt. We may even change how much time we want to wait for the next repeat of the current loop by setting self.s_dt. These three cases are demonstrated below.

Changing the waiting time on the fly


Variable Repeat[edit | edit source]

What if we want to repeat different number of times for an inner loop in respect to each repetition of the outer loop? We can do this by changing the number of repeat variable of the next column as follows:

Modifying number of repeat in the inner loop


Mirror[edit | edit source]

Very frequently we may want a particular variable to be "mirrored" to a value in consecutive loop executions (which may execute many times or only two times depending on what you want to do with it). We can use a script to implement that, as shown below (remember that i starts from 0 and increases up to self.s_n - 1):

A variable that alternates between two values


However, sometimes we may want the entire pattern to be mirrored rather than one parameter in one of the loop for a certain loop. For that we may store the value of i in one variable on the next column, then at the last column before output we can perform the mirroring using the script above, but with i replaced by that variable stored.

A script that alternates between two values can be implemented in very similar manner, and the details are omitted.

Translations and Rotation[edit | edit source]

How can translation and rotation of coordinates done for a 2d point (self.x, self.y)? We may write a script for it as well, see the example below:

Adding an offset and rotating the (x, y) position by origin


Because most often we may want to change the amount of translation or rotation in the script, ideally by passing a parameter into the function. However as a column script can only have "cur, next, i" passed in, how can we do this? One solution is to use dynamic function construction, where we bind a bunch of variables into the function that we define at loading time, and the passed variables may specify either a value expression or a variable name on self or next column from which the value is taken.

Script Ordering[edit | edit source]

Each column object execute its own scripts before calling spark() function on the next column object, and this lead to scripts execute in order of the column that they are assigned to. For example, for any column, scripts in column.s_script will always be run before those scripts in column.next.s_script. Further, scripts are executed in the order they are put in the field s_script, so rearranging the order of the scripts in the array self.s_script may lead to different execution results. The effect of this is most clear when dealing with many kinds of scripts simultaneously in one matrix or chain of columns. See the following matrices.

Fields Initial Values Loop #1 Loop #2
s_script - POLAR_OFFSET, AIM_AT_PLAYER
s_n - INFINITE 60
s_t -
s_dt - 10
ra 6
r 60
a
v

A matrix with the two scripts' order reversed:

Fields Initial Values Loop #1 Loop #2
s_script - AIM_AT_PLAYER, POLAR_OFFSET
s_n - INFINITE 60
s_t -
s_dt - 10
ra 6
r 60
a
v

Both matrices would spawn 60 bullets on a circle of radius 60 around origin, with POLAR_OFFSET to compute the position offset. However, the two matrices differ in the placement of AIM_AT_PLAYER script that adds an aim angle offset to the original movement angle (which would be simply 0 without the script, here we do not add the addition a = a + ra in the end). For the first matrix the aiming angle is calculated before the polar offset (which results in the circle shape of the bullets), and all bullets will move in the angle from the center of the spawning circle, which is the origin, towards the player. For the second matrix, the aim angle is calculated in the end, and therefore the bullets on the circle will aim from their each own position to the position of the player.


The fact that these scripts can be rearranged easily to form different looking patterns improves the modifiability of the pattern, and it comes with other advantages and disadvantages. One downside is simply that the debugging becomes slightly more complicated, where accessing the column variables is done inside a constructed function, involves a table lookup as well as nil value handling, and would certainly be less straight-forward than accessing local variables in the case with simple loops.


Another advantage of using columns scripts is that the code for different scripts (aiming scripts, offset scripts, etc.) become much more modular and you can have a better mental model with how each resulting pattern shape correlates with each script. For example, with the matrix above, we know it is a pattern based on polar coordinates instead of with standard coordinates just from seeing the name of the script "POLAR_OFFSET". And we know whether the aiming angle calculation is done for every circle of bullets or for every single bullet by looking at the order of those two scripts.


Further, the modularity introduced by this model also allows potential pattern creation speedup once you have decomposed common computations into scripts (as the ones shown above), and in actual pattern creation you can simply put the corresponding scripts into the matrix to handle complex computation. The scripts can usually be made flexible with some thoughts and allow lots of variations while still simple enough to be used many times, and part of this is done by choosing a good set of parameters in a script constructor.

Generality of Column Scripts[edit | edit source]

One may ask the question that whether the column scripts really are able to implement all the things that you can do with simple loops in general purpose programming languages. I will answer this with a method to convert the latter into the former. Consider the following loop.