Tutorial: Intermediate Script (Double Polygon) - Poyo-SSB/osu-playground GitHub Wiki

In this tutorial, we're going to create this script and learn about some more extensibility and mathematical ideas.

The script in question creates a cool slider with two polygonal sections. The slider shape was inspired by a slider in Monstrata's map Goodbye to a World.

Variables and Options

For this script, we're going to need a number of different variables to be able to finely control the slider shape. In the previous tutorial, we only used Vector2 variables, but we will need two more types—the float and the int.

Both of these types are numeric values. float is a floating point value with arbitrary levels of decimal precision, and int is an integer without decimal precision. Let's list out the variables we'll need:

  • [Vector2] Base center – The center of the first polygon.
  • [Float] Base radius – The radius of the first polygon.
  • [Float] Rotation – The rotational offset of the first polygon.
  • [Int] Ngon – The number of sides each polygon should have.
  • [Int] Major side count – The number of sides the first polygon should actually go through.
  • [Float] Radius multiplier – The relative size of the second polygon.
  • [Int] Minor side count – The number of sides the second polygon should go through.

Now, let's create those variables. In the previous tutorial, we used only Playground.AddVector2, but the new functions we'll use are intuitively named and used; the first parameter is the name and the second is the default value.

function start() {
    var baseCenter = Playground.AddVector2('baseCenter', new Vector2(Playground.PLAYFIELD_WIDTH / 2, Playground.PLAYFIELD_HEIGHT / 2));
    var baseRadius = Playground.AddFloat('baseRadius', 100);
    var rotation = Playground.AddFloat('rotation', 180);
    var ngon = Playground.AddInt('ngon', 10);
    var majorSideCount = Playground.AddInt('majorSideCount', 5);

    var radiusMultiplier = Playground.AddFloat('radiusMultiplier', 0.75);
    var minorSideCount = Playground.AddInt('minorSideCount', 5);
}

Simple enough. I set the default value of baseCenter to be the center of the playfield by dividing the provided width and height constants in half. I could just as easily have written new Vector2(256, 192), but the constants are there for those who don't care to remember them.

Now, let's add some options on the sidebar for these variables:

    Playground.AddOptionVector2(baseCenter, 'Base center');
    Playground.AddOptionFloat(baseRadius, 'Base radius', 0, 40, 150);
    Playground.AddOptionFloat(rotation, 'Rotation', 1, 0, 360);
    Playground.AddOptionInt(ngon, 'N-gon', 3, 16);
    Playground.AddOptionInt(majorSideCount, 'Major side count', 1, ngon);

    Playground.AddOptionFloat(radiusMultiplier, 'Radius multiplier', 2, 0, 2);
    Playground.AddOptionInt(minorSideCount, 'Minor side count', 1, ngon);

Previously, we used only the Playground.AddOptionVector2 function, which takes two parameters: the bindable variable and its displayable name. However, the new functions used for float and int variables have some extra parameters.

Playground.AddOptionFloat has three extra parameters. The first is the level of precision to give the user. If 0, the slider and text box can only set the value to whole numbers. If 1, the value can be set to one decimal place of precision. If 2, the value can be set to two decimal places, and so on and so forth. baseRadius and rotation both have a precision of 0 because such fine control is unnecessary. However, radiusMultiplier has a precision of 2 because it will be a smaller number.

The final two parameters are the minimum and maximum values for both Playground.AddOptionFloat and Playground.AddOptionInt. For example, rotation has a minimum value of 0 and a maximum of 360 (we're using degrees for the convenience of a user). However, these parameters don't just need to be numbers. For example, the majorSideCount and minorSideCount values have a maximum value of ngon because they should never go past the number of sides their polygon has in the first place.

Drawing the Slider

First things first—let's get the values for each of our variables for use. As one might be able to guess, the functions responsible for getting the variables are very similar to Playground.GetValueVector2 from the last tutorial:

function update() {
    var baseCenter = Playground.GetValueVector2('baseCenter');
    var baseRadius = Playground.GetValueFloat('baseRadius');
    var rotation = Playground.GetValueFloat('rotation');
    var ngon = Playground.GetValueInt('ngon');
    var majorSideCount = Playground.GetValueInt('majorSideCount');
    var radiusMultiplier = Playground.GetValueFloat('radiusMultiplier');
    var minorSideCount = Playground.GetValueInt('minorSideCount');
}

Great! Now it's time to do some math.

Last time, we simply drew two sliders by passing in three points each. This time, however, we have an arbitrary number of points depending on what variables the user chooses to modify. There's an easy solution to this, though:

    var points = [];

This code creates an empty array of values called points. This is immensely useful because arrays can be modified—added to, removed from, spliced, sliced, popped, and so on.

Let's begin by adding the first part of the slider—the first polygon. Immediately, we have an issue; how are we going to do something an unknown amount of times?

Meet the for loop:

for (var i = 0; i < 3; i++) {
    // Do some cool things...
}

This is probably the furthest into computer science a user of osu!Playground will need to go to be able to create really cool patterns.

The for loop might look scary at first, but it's actually quite simple. It has four parts:

  1. var i = 0; – Creates a variable called i (the index) with a value of 0.
  2. i < 3; – Checks if i is less than 5.
  3. i++ – Increments i by 1.
  4. // Do some cool things... – Is a placeholder for when we do cool things.

Here's what happens, in order:

  • Begin a for loop.
  • 1 → Create a variable i with the value 0.
  • 2 → Is i less than 3? (yes, i == 0)
  • 4 → Do some cool things...
  • 3 → Increment i by 1.
  • 2 → Is i less than 3? (yes, i == 1)
  • 4 → Do some cool things...
  • 3 → Increment i by 1.
  • 2 → Is i less than 3? (yes, i == 2)
  • 4 → Do some cool things...
  • 3 → Increment i by 1.
  • 2 → Is i less than 3? (no, i == 3)
  • Exit the loop and continue with the code.

Notice that cool things are done three times, once for each value of i starting from 0 until 3; that's 0, 1, and 2.

Note that all four parts of the for loop are nothing more than expressions on their own, so they can be replaced with anything. For example, we can use this:

    for (var i = 0; i < majorSideCount + 1; i++) {
        // Do some cool things...
    }

This will run once for every vertex that we need to draw—in this case, that's the number of sides specified plus one. Before we do that, let's first define a point to rotate.

    var basePoint = add(baseCenter, new Vector2(baseRadius, 0));
    for (var i = 0; i < majorSideCount + 1; i++) {
        // Do some cool things...
    }

In this case, we use a function defined globally by osu!Playground, add. This is the annoying part of JavaScript—it provides no way to use normal mathematical operators on external types (e.g. Vector2), so we're stuck with functions like add.

The point itself is just the baseCenter variable shifted to the right by baseRadius osu!Pixels. Now, we can add the points to the first polygon.

    var basePoint = add(baseCenter, new Vector2(baseRadius, 0));
    for (var i = 0; i < majorSideCount + 1; i++) {
        points.push(rotate(basePoint, baseCenter, i * 2 * Math.PI / ngon));
    }

This looks like a mess of parentheses and operators, so let's work our way out from the deepest function.

First of all, we rotate our newly-defined basePoint around the user's baseCenter by a factor of i * 2 * Math.PI / ngon. Recall that 2π = 360°, so this is just multiplying i times 360° divided by sides in our polygon. That means that when i is 0, no rotation occurs. When i is equal to ngon, a full 360° rotation occurs.

Then, we take the result of the rotation, and push it into the points array we created earlier. The points array has nothing special about it, but we'll use it later.

You might notice that something's missing. We haven't yet added the rotation value—it currently affects nothing. To use it, let's first convert its value into radians and store it in a variable called offset. Then, we can add offset to the angle value in the rotate function:

    var basePoint = add(baseCenter, new Vector2(baseRadius, 0));
    var offset = rotation / 180 * Math.PI;
    for (var i = 0; i < majorSideCount + 1; i++) {
        points.push(rotate(basePoint, baseCenter, offset + i * 2 * Math.PI / ngon));
    }

With that, the first part of our slider is complete. To start drawing the second part, we'll need to do some math. Let's first begin by defining some references to the points we'll use:

    basePoint = points[points.length - 1];
    var previousPoint = points[points.length - 2];

We don't use var with basePoint because we already defined it earlier; we're just redefining it now. The syntax we used here is called "indexing" and is done to reference elements in a collection. In this case, the collection is our array of points. For example, take this array:

    var array = [
        "Barack Obama",
        "Cookiezi",
        "Bozo the Clown"
    ];

The expression array[0] accesses the array and returns the element at the index 0. In this case, that's "Barack Obama". array[1] returns "Cookiezi", and array[2] returns "Bozo the Clown". However, if the index is out of range—for example, using array[3]—an error is thrown and our code ceases to run.

Arrays in JavaScript have several useful functions and properties built-in to them. The property we use here is Array.length, which represents the number of elements in the array. The example array has a length value of 3. To get the element with the highest index—in other words, the last element—we use array[array.length - 1]. We're simply taking the element that has an index one less than the length of the array.

Back to our osu!Playground code. We use this sort of operation twice. The first one is to redefine basePoint as the last element of the array, our most recently-added point. The second one is to set previousPoint as the second-to-last element. We'll use this to do some trigonometry.

Now's where we start getting more into math than computer science.

What we intend to do here is repeat the process we used to create the first polygonal part. That is, we rotate a point around the center a certain number of times.

However, this time, we need to first calculate where that center should be since it's entirely dependent on the other parameters before it.

First, we calculate the angle between a side of the polygon to the center. That's an interior angle of the polygon divided in half (α):

    var radiusAngle = (Math.PI - (Math.PI / ngon)) / 2;

Secondly, we calculate the angle of the final point of our first polygon relative to the x-axis (β). We do this using a function defined in many modern programming languages: Math.atan2. The function is similar to the standard trigonometric arctangent function but takes into account the signs of the parameters to determine the proper quadrant.

    var sideAngle = Math.atan2(basePoint.y - previousPoint.y, basePoint.x - previousPoint.x);

Finally, we calculate the radius of the second polygon (r1):

    var newRadius = baseRadius * radiusMultiplier;

Now we have all the values we need to calculate the new center. First, we create a point to the left of basePoint with a distance of newRadius:

    var newCenter = subtract(basePoint, new Vector2(newRadius, 0));

Then, we rotate it by the angle of the final side (β):

    newCenter = rotate(newCenter, basePoint, sideAngle);

The usage of Math.atan2 comes in handy here because it accounts for variable quadrants and the strangeness of osu!'s coordinate system. After this is done, newPoint is on the line between basePoint and previousPoint, ready to be rotated to meet the new center:

    newCenter = rotate(newCenter, basePoint, radiusAngle);

And that's all the math we need! Finally, we can draw the second polygon using a for loop as we did with the first.

    for (var i = 0; i < minorSideCount + 1; i++) {
        points.push(rotate(basePoint, newCenter, i * 2 * Math.PI / -ngon));
    }

Note that ngon is negative—this is to change the direction in which the polygon is drawn.

With that, our double polygon slider is finally complete! All that's left is to draw it:

    Playground.AddSlider(CurveType.Linear, points);

And our script is done! The final script made in this tutorial is available here.

⚠️ **GitHub.com Fallback** ⚠️