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.
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.
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:
-
var i = 0;
– Creates a variable calledi
(the index) with a value of 0. -
i < 3;
– Checks ifi
is less than 5. -
i++
– Incrementsi
by 1. -
// 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.