Please note that I don't support this anymore. Check osbpy for 100% liberate storyboarding.
Hi everyone! I would like to introduce my storyboarding guide to you. I would like to focus on SGL coding, which is basically "programming" language developed by MoonShade and forked by Damnae. Work with SGL is very similar to any other programming language, that means it will be a little harder for newbies, however, I will try to explain all basics, so you understand the logic and then all functions SGL provides. However, I will also make one part about optimizing storyboard in overall, both SGL and manual storyboards.
To start coding the storyboards, make sure to:
- Own Damnae's release of SGL Editor. MoonShade's version is outdated and is missing some functions implemented in this release.
- Map and images, so the storyboard is applicable.
- Logic skills (eventually math a little)
To understand SGL, I need to explain some basics of regular programming. I will only mention things that work in SGL, these don't apply for every programming language, but to avoid confusion, I will explain only things that work here and are actually useful for making a storyboard.
All programming languages have multiple data types. They are used to define whether some value is a number, text, binary and so on. Here is a little list of commonly used data types:
- int - integer, used for numbers without fractional component (e.g. 2, 4, 6)
- float - real number, used for numbers with fractional component (e.g. 3.1415, 2.85, 1.01)
- bool - does have only 2 values, "true" or "false" - it's usage is probably obvious.
- string - text
- object - special for SGL, defines a sprite (static object in storyboard) or animation
Basic form: dataType name = value;
int cats = 2;
float pi = 3.1415;
However, there is a little difference in SGL. Most programming languages have a special variable data type, which is called var. This data type automatically sets the data type to "correct" one, depending on what we enter in. SGL can use only this type! This makes the language a little simpler - you don't have to guess which data type it is, but on the other side it might cause issues. Though it converts to type which it needs at the moment, it doesn't always know what you want to convert to. Common issue is when dividing numbers:
var firstnumber = 3
var secondnumber = 10
firstnumber = firstnumber/secondnumber //Result is 3/10 = 0
Above, we want to change value of firstnumber to its value divided by second number, which is 10. That means 3/10. However, the result will be 0, because the language thinks that both are integers! If we find a way to tell him at least one of these numbers is float, it will change firstnumber to flat and thus result will be able to use fractional component. Like below.
var firstnumber = 3 //firstnumber is now considered as integer
var secondnumber = 10.0 //however secondnumber is considered as float because of .0
firstnumber = firstnumber/secondnumber //* Result is 3/10 = 0.3 because one of numbers was float, thus it converted firstnumber to float as well, thus can have fractional component. */
int cats = 2;
float pi = 3.1415;
However, there is a little difference in SGL. Most programming languages have a special variable data type, which is called var. This data type automatically sets the data type to "correct" one, depending on what we enter in. SGL can use only this type! This makes the language a little simpler - you don't have to guess which data type it is, but on the other side it might cause issues. Though it converts to type which it needs at the moment, it doesn't always know what you want to convert to. Common issue is when dividing numbers:
var firstnumber = 3
var secondnumber = 10
firstnumber = firstnumber/secondnumber //Result is 3/10 = 0
Above, we want to change value of firstnumber to its value divided by second number, which is 10. That means 3/10. However, the result will be 0, because the language thinks that both are integers! If we find a way to tell him at least one of these numbers is float, it will change firstnumber to flat and thus result will be able to use fractional component. Like below.
var firstnumber = 3 //firstnumber is now considered as integer
var secondnumber = 10.0 //however secondnumber is considered as float because of .0
firstnumber = firstnumber/secondnumber //* Result is 3/10 = 0.3 because one of numbers was float, thus it converted firstnumber to float as well, thus can have fractional component. */
As the name says, comments are some notes which you can add to the code. It does not have any effect on function, but it is very good to include them in your code. They help you to organize code better way - in case you share SGL code, people may be confused if you don't use comments, same can happen to you if you have to change something in storyboard and cannot orientate in your own code. There are two types of comments:
- // - Single line comment, text on another line won't be comment but part of code!
- /* */ - Block comment, all text between these marks is considered a comment.
This is not a comment
/* Above line will be considered as code, thus will be compiled, that will give you an error.
this line is still a comment and will work perfectly fine. */
Obviously, even SGL does have mathematical, logical and compare operators. Mathematical operators don't need a lot of explaining, so let's start with them:
- + - Merges value of left and right side. (If numerical, then adds right to left)
- - - Subtracts value of right side from left side.
- / - Divides left value by right value.
- * - Multiplies value of left side with value of right side.
- % - Divides left value by right value, but gives back the remaining value.
- In other programming languages, you can use +=, /=, *=, -= instead of = to apply "name = name <operator before => name2". This won't work in SGL, so yes, you always have to write what you want to merge, divide etc., there is no shorter way.
- > - Checks whether left side's value is higher than right
- >= - Checks whether left side's value is higher or equal to right
- < - Checks whether left side's value is lower than right
- <= - Checks whether left side's value is lower or equal to right
- == - Checks whether left side's value is equal right
- != - Checks whether left side's value is not equal right
- && - If all connected expressions are true, operation will continue.
- || - If at least one expression is true, operation will continue.
Conditional expressions, or just conditions execute some code if logical expression is true. It can be extended by else or else if as many times as you want. Here's example:
Basic form:
if (value1 comparation value2)
{
//Commands to perform if condition in bracket is true
}
Extended form:
if (value1 comparation value2)
{
//Commands to perform if condition in brackets is true
}
else if (value1 comparation value2)
{
//Commands to perform if condition in previous brackets is false, but condition in these brackets is true
}
else
{
//Commands to perform if none of above conditions was true
}
...
You can extend this infinitely, depending on your needs. There does not even have to be "else if", if there are only two options.
var i = 20
var y = 40
if (i == y)
{
//If it was true, code in these curly brackets would run, but 20 is not equal to 40, so it won't run and will continue in code.
}
else if (2 * i == y && i != y)
{
/* If above condition is true, code in brackets will run. 2 * i = 2 * 20 = 40. Right side is y, which is also 40. That means condition first condition is true. && means that next expression must be true as well - it is asking whether i is different from y. 20 is not equal to 40, so it is true as well. Commands in these curly brackets will execute. */
}
else
{
//If none of above was true, commands in these brackets would run, however, above condition was true, so it won't run this code.
}
if (value1 comparation value2)
{
//Commands to perform if condition in bracket is true
}
Extended form:
if (value1 comparation value2)
{
//Commands to perform if condition in brackets is true
}
else if (value1 comparation value2)
{
//Commands to perform if condition in previous brackets is false, but condition in these brackets is true
}
else
{
//Commands to perform if none of above conditions was true
}
...
You can extend this infinitely, depending on your needs. There does not even have to be "else if", if there are only two options.
var i = 20
var y = 40
if (i == y)
{
//If it was true, code in these curly brackets would run, but 20 is not equal to 40, so it won't run and will continue in code.
}
else if (2 * i == y && i != y)
{
/* If above condition is true, code in brackets will run. 2 * i = 2 * 20 = 40. Right side is y, which is also 40. That means condition first condition is true. && means that next expression must be true as well - it is asking whether i is different from y. 20 is not equal to 40, so it is true as well. Commands in these curly brackets will execute. */
}
else
{
//If none of above was true, commands in these brackets would run, however, above condition was true, so it won't run this code.
}
If you understood conditions, loops should not be hard at all. If I wanted to be very simple, it is exactly the same as conditions, but it repeats commands as long as the condition is true. Another difference is there is no "else", that is only for "if" condition.
While loop:
while (value1 comparation value2)
{
//Commands to perform AND REPEAT while condition is true
}
Very important thing is to avoid closed loop. Respectively, the condition must once stop being true, otherwise it will generate code infinitely. tl;dr SGL will shut down. To ensure the condition will once be false, we usually change left value (increase or decrease). For example:
var i = 0
while (i < 10)
{
//Commands to perform and repeat as long as i is lower than 10
i++;
}
i++ means i will be increased by 1. That means commands in curly brackets will run 10, because the loop does have to run that many times before i will not be lower value than 10. Obviously, you can increase it also by "i = i + 1" or any way you like.
For loop:
for (initialization;condition;increment)
{
//Commands to perform AND REPEAT while condition is true
}
This is very similar to while, but on the other side, you don't have to create anything before the loop and it is increasing its value automatically, so the i++ and similar things are not needed. It might seem complicated, but believe it or not, it is pretty easy thing.
for (var i = 0;i < 10;i++)
{
//Commands to perform and repeat as long as i is lower than 10
}
First command in brackets is creating a new var and giving it value of 0 on first start of the loop (doesn't happen every time the loop repeats). Second command in brackets is the condition what loop is checking for - whether i is lower than 10. Third command in brackets is command that is performed at the end of each loop, that means it increases i by 1 each loop.
tl;dr
1st command in brackets = creates i and gives it value first time the loop starts
2nd command = works as while loop, checks whether it is true and while it is, it will loop the commands in curly brackets
3rd command = increases value of i
while (value1 comparation value2)
{
//Commands to perform AND REPEAT while condition is true
}
Very important thing is to avoid closed loop. Respectively, the condition must once stop being true, otherwise it will generate code infinitely. tl;dr SGL will shut down. To ensure the condition will once be false, we usually change left value (increase or decrease). For example:
var i = 0
while (i < 10)
{
//Commands to perform and repeat as long as i is lower than 10
i++;
}
i++ means i will be increased by 1. That means commands in curly brackets will run 10, because the loop does have to run that many times before i will not be lower value than 10. Obviously, you can increase it also by "i = i + 1" or any way you like.
For loop:
for (initialization;condition;increment)
{
//Commands to perform AND REPEAT while condition is true
}
This is very similar to while, but on the other side, you don't have to create anything before the loop and it is increasing its value automatically, so the i++ and similar things are not needed. It might seem complicated, but believe it or not, it is pretty easy thing.
for (var i = 0;i < 10;i++)
{
//Commands to perform and repeat as long as i is lower than 10
}
First command in brackets is creating a new var and giving it value of 0 on first start of the loop (doesn't happen every time the loop repeats). Second command in brackets is the condition what loop is checking for - whether i is lower than 10. Third command in brackets is command that is performed at the end of each loop, that means it increases i by 1 each loop.
tl;dr
1st command in brackets = creates i and gives it value first time the loop starts
2nd command = works as while loop, checks whether it is true and while it is, it will loop the commands in curly brackets
3rd command = increases value of i
SGL offers you two ways to place objects on certain time - first is using absolute offset, second is using relative offset. Difference is, you set exact start and end for absolute offset and object will start and end on millisecond you choose. However relative offset can be used so you set only how long time is the object visible and overall offset which will delay start of all objects. May be useful, but if you ask me, it just works as avoiding elementary school math, thus I haven't used it yet, but just so you know it is available.
Basic form:
at (offset)
{
/* Commands to perform at offset in brackets (enter only number, it is considering milliseconds, you can for example copy this value with CTRL+C in Design in osu! editor) */
}
at (offset)
{
/* Commands to perform at offset in brackets (enter only number, it is considering milliseconds, you can for example copy this value with CTRL+C in Design in osu! editor) */
}
If you already know basics of programming or read previous section, you should be fine to continue with this section. Most of above is similar in many programming languages, following will be functions specific for SGL.
Before moving with an object, you obviously have to create it. There are two methods, one for normal image, second for animation (built from multiple frames like picture0.jpg, picture1.jpg, picture2.jpg etc.)
Regular:
var customName = new Sprite(Path, Layer, Origin);
Animation:
var customName = new Animation(Path, Layer, Origin, Frames, Delay, LoopType);
Before I give examples, we need to explain certain values you are going to use here:
We want both images in foreground and with TopLeft origin. Animation does have 3 frames and will swap image each 100ms, is going to repeat over and over.
var myimage = new Sprite("dot.png", Foreground, TopLeft);
var mycuteanimation = new Animation("effect.png", Foreground, TopLeft, 3, 100, LoopForever);
var customName = new Sprite(Path, Layer, Origin);
Animation:
var customName = new Animation(Path, Layer, Origin, Frames, Delay, LoopType);
Before I give examples, we need to explain certain values you are going to use here:
- Path = Relative path to the image (e.g. "image.png")
- Layer = Z-Axis group. It means that if object is in Background, it will be covered by things in Foreground. However priority in certain group is determined by when order in code.
- Foreground
- Background
- Fail
- Pass
- Origin = Point of image that all changes are relative to. For example, if you move image on 200,200 while it will be TopLeft, it will move top left corner of the image here and so on, you are only moving with point and the image is relative to the point. However, everything can be done with any of origins, only vector scale cannot be avoided, it "rolls" the image towards the origin point.
- TopLeft
- TopCentre
- TopRight
- CentreLeft
- Centre
- CentreRight
- BottomLeft
- BottomCentre
- BottomRight
- Frames = How many pictures does animation have. e.g. you use pic0.jpg, pic1.jpg, pic2.jpg, so you type 3.
- Delay = How long time does it take before one frame swaps to another (in milliseconds)
- LoopTime = Determines whether the animation should repeat over and over, or run once and stay on last frame.
- LoopForever = Loops
- LoopOnce = Stops on last frame
We want both images in foreground and with TopLeft origin. Animation does have 3 frames and will swap image each 100ms, is going to repeat over and over.
var myimage = new Sprite("dot.png", Foreground, TopLeft);
var mycuteanimation = new Animation("effect.png", Foreground, TopLeft, 3, 100, LoopForever);
Methods are actual actions with objects in storyboard. I will list all functions there, including some non-objects, just to avoid additional sections.
Calling function:
objectName.<function>(parameters);
List of available functions:
var spr = new Sprite("img.jpg",Foreground,TopLeft); //Creates an object for img.jpg
spr.move(500,1000,100,200,200,200); //On 500 till 1000 move object from 100;200 to 200;200
spr.fade(500,0.5); //Sets default opacity of the object to 0.5 on 500
Here is another example, this time in a loop:
var time = 500; //Basic offset of 500, used in loop and increased by 500 each step.
while(time < 10000)
{
var spr = new Sprite("img.jpg",Foreground,TopLeft); //Creates an object for img.jpg
spr.move(time,time + 500,100,200,200,200); //Similar as previous example, this time I use time from var time.
spr.fade(time,0.5); //Similar as previous example, this time I use time from var time.
time = time + 500; //Increases time as long as it is lower then 10000, then ends the loop.
}
SB Loops:
Storyboard loops are last element in osu! storyboard functions. You probably know how does loop work now, so there is just the difference this is directly performed by osu!, not by many generated lines in .osb. Another loop can be triggered on some action:
var spr = new Sprite("img.jpg",Foreground,TopLeft);
spr.startTriggerLoop(Failing,0,80000);
spr.fade(0,500,0,1); //If failing, image will fade-in within 500ms.
spr.endLoop();
spr.startTriggerLoop(Passing,0,80000); //If not failing, image will fade-out within 500ms.
spr.fade(0,500,1,0);
spr.endLoop();
objectName.<function>(parameters);
List of available functions:
- Movement on X and Y: move(easing, startOffset, endOffset, Xstart, Ystart, Xstop, Ystop);
- easing = 0 (none), 1 (fast start), 2 (fast end) - Is 0 if not set.
- startOffset and endOffset = Start and end time in milliseconds
- Xstart, Ystart - Position on axis X and Y on start
- Xend, Yend - Position on axis X and Y on end
- Separate movement on X: moveX(easing, startOffset, endOffset, Xstart, Xend);
- Separate movement on Y: moveY(easing, startOffset, endOffset, Ystart, Yend);
- Fade: fade(easing, startOffset, endOffset, FadeStart, FadeEnd);
- Scale: scale(easing, startOffset, endOffset, ScaleStart, ScaleEnd);
- Rotate: rotate(easing, startOffset, endOffset, RotationStart, RotationEnd);
- Rotation value is in radians, thus 360° rotation is 6.283185, use maths to get other values. Also using that long number is insensible, you don't have to be extra precise.
- Color: color(easing, startOffset, endOffset, RedStart, GreenStart, BlueStart, RedEnd, GreenEnd, BlueEnd);
- Values of colors can have value from 0 to 255
- Vector Scale: scaleVec(easing, startOffset, endOffset, XScaleStart, YScaleStart, XScaleEnd, YScaleStart);
- The way where the image "scrolls" doesn't depend on this commands settings. It depends on origin of created object (TopLeft, Centre etc.)
- Easing and ALL "end" values don't have to be set, if there is no time dependent change. In case object is visible for whole time of map, you don't even have to use startOffset.
- Horizontal Flip: flipH(startOffset, endOffset);
- Vertical Flip: flipV(startOffset, endOffset);
- Additive: additive(startOffset, endOffset);
- You can create a random number by using rand(lowRange,highRange); - This can be useful if you want random positioning or random scale.
var spr = new Sprite("img.jpg",Foreground,TopLeft); //Creates an object for img.jpg
spr.move(500,1000,100,200,200,200); //On 500 till 1000 move object from 100;200 to 200;200
spr.fade(500,0.5); //Sets default opacity of the object to 0.5 on 500
Here is another example, this time in a loop:
var time = 500; //Basic offset of 500, used in loop and increased by 500 each step.
while(time < 10000)
{
var spr = new Sprite("img.jpg",Foreground,TopLeft); //Creates an object for img.jpg
spr.move(time,time + 500,100,200,200,200); //Similar as previous example, this time I use time from var time.
spr.fade(time,0.5); //Similar as previous example, this time I use time from var time.
time = time + 500; //Increases time as long as it is lower then 10000, then ends the loop.
}
SB Loops:
Storyboard loops are last element in osu! storyboard functions. You probably know how does loop work now, so there is just the difference this is directly performed by osu!, not by many generated lines in .osb. Another loop can be triggered on some action:
- Start of loop:
- Normal loop: startLoop(startOffset, loops);
- Trigger loop: startTriggerLoop(trigger, startOffset, endOffset);
- Actions in loop are relative to startOffset, using absolute time is impossible. Start time of any method (move, scale etc.) will be increased by start of the loop.
- Triggers: Passing, Failing, HitSoundClap, HitSoundFinish, HitSoundWhistle
- End of loop: endLoop();
var spr = new Sprite("img.jpg",Foreground,TopLeft);
spr.startTriggerLoop(Failing,0,80000);
spr.fade(0,500,0,1); //If failing, image will fade-in within 500ms.
spr.endLoop();
spr.startTriggerLoop(Passing,0,80000); //If not failing, image will fade-out within 500ms.
spr.fade(0,500,1,0);
spr.endLoop();
Last thing to explain here is creation of functions. Functions are blocks of commands that can be called with custom parameters, which will be used in the function. These are mainly useful to make your code clean and well-arranged.
Creating the function:
function customName(parameters)
{
//Commands to perform
}
Executing the function:
customName(parameters);
Before you can call the function, it must be created. There can be as many custom parameters as you want and these are usually used in commands to replace some value. For example:
function light(timestart, timeend, valuestart, valueend)
{
var lightpic = new Sprite("light.png",Foreground,Centre);
lightpic.fade(timestart, timeend, valuestart, valueend); //Takes timestart, timeend etc. and puts here values which were defined in brackets below.
}
light(100, 1000, 0, 0.5); /* Puts values from brackets to timestart, timeend, valuestart, valueend in the order you described it in "function light(....)" */
light(1000, 2000, 0, rand(1,10)/10.0); /* Puts values from brackets to timestart, timeend, valuestart, valueend in the order you described it in "function light(....)" */
function customName(parameters)
{
//Commands to perform
}
Executing the function:
customName(parameters);
Before you can call the function, it must be created. There can be as many custom parameters as you want and these are usually used in commands to replace some value. For example:
function light(timestart, timeend, valuestart, valueend)
{
var lightpic = new Sprite("light.png",Foreground,Centre);
lightpic.fade(timestart, timeend, valuestart, valueend); //Takes timestart, timeend etc. and puts here values which were defined in brackets below.
}
light(100, 1000, 0, 0.5); /* Puts values from brackets to timestart, timeend, valuestart, valueend in the order you described it in "function light(....)" */
light(1000, 2000, 0, rand(1,10)/10.0); /* Puts values from brackets to timestart, timeend, valuestart, valueend in the order you described it in "function light(....)" */
Now, you should know some basics and if you apply logic, you should be able to make some nice stuff. However, nothing goes easily without issues and you have to be aware that you may make some. The most important is to admit it and trying to make a solution - Always save your code, if issue is found in your storyboard, it may need to change one line in code - which may mean thousand lines in .osb, so deleting your SGL code because you think the storyboard is done may cause you troubles later. I will regularly update this section and I will mention common things you could and if possible should avoid.
High SB load because of rendering out of screen
This is pretty common issue and many people don't even know about it, they blame lags on object count on screen, but they don't know the issue is things being rendered when it is not necessary. There is a pretty nice example of this issue - I am not criticizing the map in any way, I am just using it as an example, nothing else.
At first look, it looks pretty cool, this is a view which is given to the player:
However, if you investigate the map more in-depth, you will find out that objects wait till they should move off-screen and this way they are rendered when not needed.
Note that objects still load even if they are out of screen. All of these objects have some fade on 80702, like 0.8 - 0.9 or something. That means they will be rendered since that time. You should always give object settings like scale, fade, etc. on the time when they should be visible, otherwise you will increase the load multiple times. In that map, objects did stop on left side and waited for nothing, which is even worse (causes the same load, but if something is not going to do any action, fade it so it is not visible). Worst scenario is when these objects are hidden and their offset is 0.
Here is a reason why this happens and a solution for it:
var i = 0;
var delay = 1000;
while(i < 1000)
{
var light = new Sprite("light.png");
light.fade(80702, 0.8); //fade is set to 0.8 on 80702, but it should be "80702 + delay" so it starts at same time as light.move
//Case when it would be on 0 offset is: light.fade(0.8); - Don't use it if you aren't 100% sure you need it visible in start.
light.move(80702 + delay, 80702 + delay + 2000, 700, 200, -120, 200); //Moves the object from 700;200 to -120;200
delay = delay + 300; //Increases delay so every next object appears 300ms later than this.
i++;
}
Solution:
light.fade(80702, 0.8); ---> light.fade(80702 + delay, 0.8);
//Also should have some fade end.
Useless things in .osb
This is more like issue of manual storyboards, that are actually simple. People add completely useless things to their .osb. If it is generated with SGL, avoiding it is almost impossible, but for simple storyboards you should make it look clean for modders. For example, may people disable background this way:
Sprite,Background,Centre,"BG.png",320,240
M,0,77,,320,240
F,0,77,,0
Obviously, M,0,77,,320,240 is useless completely. If object doesn't change position progressively, you may enter it in its first line, after "BG.png",... this number is position, this makes code little cleaner for modding. F,0,77,,0 is useless as well, because if you don't use any commands for object, it will be invisible anyway.
Solution:
Rewrite all 3 lines to: Sprite,Background,Centre,"BG.png",320,240
Wrong full-screen scale
9 cases out of 10 when I check storyboard, I see a wrong scale on objects that are supposed to cover whole screen. This renders object out of screen, decreases its quality by "zooming" it more than needed. Key to avoid this is firstly: Always have background image you want to use in same aspect ratio as storyboard (with Widescreen support, it should be 16:9, without widescreen support, it should be 4:3). Preferably you should have 1366x768 background image as it is the highest allowed, thus will result in the highest quality possible.
People often scale it so it is scaled over whole width of editor. However editor is not same width as storyboard. For example:
You may say it is not scaled over whole screen, however, in gameplay, it is. Now, how to count scale amount. We know that storyboard can be either 640x480 or 853x480, so we have to make the background image to adapt to height of the storyboard, you can count it like this:
Image: 1366x768
Storyboard: 853x480
Scale: 480/768 = 0.625
After counting this, you should add any scale to your image (on time where it should be visible, not on offset 0), and find it in .osb, it may look like this:
S,0,79402,,0.9848951 (0.9... was random scale created in "Design", by adding Scale point)
Here, you rewrite the number to our scale, which we counted above:
S,0,79402,,0.625
And that's it, image is scaled over whole storyboard, and doesn't cut itself anymore.
Frequency spectrum
If you want fully automated spectrum, be sure to check XinCrin's spectrumGenerator! If you want to be less limited and make it do 100% what you want, read below.
Some people find this kind of cool and are asking for how is it done quite frequently. Don't ask which language can do this - We may answer you almost anything, for example Lua. There's nothing like "this language is good for it, this isn't", it's almost equal and depends on your skills. Probably the easiest to run will be this one. Note that I won't (at least at this point) make a program that will do everything for you, but I will explain how easy it is for example in Java. Why Java? Well, probably because newbies will understand this much more easily and as I already mentioned, there are examples of works, that allows you to add about 10 lines of code, remove few lines and have an output. I don't know how about Eclipse, but in NetBeans, you can choose ChartAudioBar in examples for JavaFX. Like picture below.
It's obviously not the correct way to do this, but I want to prove how easily it can be done. Compare the code in NetBeans with this little modification. Read the comments properly. It says all changes and explains most of things that were used.
If you are not interested in taking a look at the Java code, you may just get executable file. Use it by putting mp3.mp3 file to folder with it and run it. Then wait till song ends. Now, why the heck would I use Ruby, not Java. First thing is, Java is less tolerant, making the code in Ruby is very flexible, you don't need to care about data types etc. it is just more comfortable for so simple applications. Second reason is that you don't need to compile anything, you have one code and it goes line by line. Doesn't require any thinking. Third reason is the fact that some people will understand the code and may make nice things with it with just little knowledge - That means you can send only data.txt to someone and he may change and run the code just so it makes what you want. Last reason is that if there is someone to do this, he may run the code through Ruby on Rails - Means that you'll be able to get your storyboard in your internet browser. Only disadvantage I can see here is that people will be too lazy to install Ruby, but same may apply for any other language apart from supported languages by default. If you have any trouble, don't worry and contact me.
If you ask me, why not everything in Ruby? I wanted to prove that anyone can do it by editing some template. Moreover, it's because I would force people to install some gems and I think most people use Windows here, so some gems could be disaster. I may later do this in other languages, for example Python, but I don't think GUI is ever coming as it's not that much customizable, so I would like to avoid it.
This is pretty common issue and many people don't even know about it, they blame lags on object count on screen, but they don't know the issue is things being rendered when it is not necessary. There is a pretty nice example of this issue - I am not criticizing the map in any way, I am just using it as an example, nothing else.
At first look, it looks pretty cool, this is a view which is given to the player:
However, if you investigate the map more in-depth, you will find out that objects wait till they should move off-screen and this way they are rendered when not needed.
Note that objects still load even if they are out of screen. All of these objects have some fade on 80702, like 0.8 - 0.9 or something. That means they will be rendered since that time. You should always give object settings like scale, fade, etc. on the time when they should be visible, otherwise you will increase the load multiple times. In that map, objects did stop on left side and waited for nothing, which is even worse (causes the same load, but if something is not going to do any action, fade it so it is not visible). Worst scenario is when these objects are hidden and their offset is 0.
Here is a reason why this happens and a solution for it:
var i = 0;
var delay = 1000;
while(i < 1000)
{
var light = new Sprite("light.png");
light.fade(80702, 0.8); //fade is set to 0.8 on 80702, but it should be "80702 + delay" so it starts at same time as light.move
//Case when it would be on 0 offset is: light.fade(0.8); - Don't use it if you aren't 100% sure you need it visible in start.
light.move(80702 + delay, 80702 + delay + 2000, 700, 200, -120, 200); //Moves the object from 700;200 to -120;200
delay = delay + 300; //Increases delay so every next object appears 300ms later than this.
i++;
}
Solution:
light.fade(80702, 0.8); ---> light.fade(80702 + delay, 0.8);
//Also should have some fade end.
Useless things in .osb
This is more like issue of manual storyboards, that are actually simple. People add completely useless things to their .osb. If it is generated with SGL, avoiding it is almost impossible, but for simple storyboards you should make it look clean for modders. For example, may people disable background this way:
Sprite,Background,Centre,"BG.png",320,240
M,0,77,,320,240
F,0,77,,0
Obviously, M,0,77,,320,240 is useless completely. If object doesn't change position progressively, you may enter it in its first line, after "BG.png",... this number is position, this makes code little cleaner for modding. F,0,77,,0 is useless as well, because if you don't use any commands for object, it will be invisible anyway.
Solution:
Rewrite all 3 lines to: Sprite,Background,Centre,"BG.png",320,240
Wrong full-screen scale
9 cases out of 10 when I check storyboard, I see a wrong scale on objects that are supposed to cover whole screen. This renders object out of screen, decreases its quality by "zooming" it more than needed. Key to avoid this is firstly: Always have background image you want to use in same aspect ratio as storyboard (with Widescreen support, it should be 16:9, without widescreen support, it should be 4:3). Preferably you should have 1366x768 background image as it is the highest allowed, thus will result in the highest quality possible.
People often scale it so it is scaled over whole width of editor. However editor is not same width as storyboard. For example:
You may say it is not scaled over whole screen, however, in gameplay, it is. Now, how to count scale amount. We know that storyboard can be either 640x480 or 853x480, so we have to make the background image to adapt to height of the storyboard, you can count it like this:
Image: 1366x768
Storyboard: 853x480
Scale: 480/768 = 0.625
After counting this, you should add any scale to your image (on time where it should be visible, not on offset 0), and find it in .osb, it may look like this:
S,0,79402,,0.9848951 (0.9... was random scale created in "Design", by adding Scale point)
Here, you rewrite the number to our scale, which we counted above:
S,0,79402,,0.625
And that's it, image is scaled over whole storyboard, and doesn't cut itself anymore.
Frequency spectrum
If you want fully automated spectrum, be sure to check XinCrin's spectrumGenerator! If you want to be less limited and make it do 100% what you want, read below.
Some people find this kind of cool and are asking for how is it done quite frequently. Don't ask which language can do this - We may answer you almost anything, for example Lua. There's nothing like "this language is good for it, this isn't", it's almost equal and depends on your skills. Probably the easiest to run will be this one. Note that I won't (at least at this point) make a program that will do everything for you, but I will explain how easy it is for example in Java. Why Java? Well, probably because newbies will understand this much more easily and as I already mentioned, there are examples of works, that allows you to add about 10 lines of code, remove few lines and have an output. I don't know how about Eclipse, but in NetBeans, you can choose ChartAudioBar in examples for JavaFX. Like picture below.
It's obviously not the correct way to do this, but I want to prove how easily it can be done. Compare the code in NetBeans with this little modification. Read the comments properly. It says all changes and explains most of things that were used.
Code
This is unreadable on website, it's better to paste in NetBeans or other program that supports Java coding. However, we still have only values of the spectrum, so we need to convert it to storyboard-compatible language. That can be done in any programming language. The point is, you have to make it yourself to make it work according to your imagination. In this example, I will use Ruby, which should be easy to edit as you want - Later, I'll maybe convert it to other languages or even make some GUI for it, but you should be able to make it as customized as you want if you use this. Here's a GitHub link for it.package chartaudiobar;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.media.AudioSpectrumListener;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.stage.Stage;
public class ChartAudioBar extends Application {
private XYChart.Data<String, Number>[] series1Data;
private AudioSpectrumListener audioSpectrumListener;
//We don't need this line, because we need to use local file. private static final String AUDIO_URI = System.getProperty("demo.audio.url","http://download.oracle.com/otndocs/products/javafx/oow2010-2.flv");
private static MediaPlayer audioMediaPlayer;
//We don't need this as well as it's equally useless as previous thing. private static final boolean PLAY_AUDIO = Boolean.parseBoolean(System.getProperty("demo.play.audio","true"));
//We anyway need output of the default chart, so let's say we'll put it to file data.txt, hence the 'import java.io.File' needs to be added to imports.
private static File filedata = new File("data.txt");
private void init(Stage primaryStage) {
Group root = new Group();
primaryStage.setScene(new Scene(root));
root.getChildren().add(createChart());
audioSpectrumListener = new AudioSpectrumListener() {
@Override public void spectrumDataUpdate(double timestamp, double duration,
float[] magnitudes, float[] phases) {
for (int i = 0; i < series1Data.length; i++) {
series1Data[i].setYValue(magnitudes[i] + 60);
//Here we need to add code to write our data to data.txt file
//We can see it operates with timestamp, duration, magnitudes and phases - For osu! spectrum, we just need time, magnitude and harmonic (index, so we will use i)
try {
FileWriter fileWritter = new FileWriter(filedata.getName(),true); //Loading the file for writing
BufferedWriter bufferWritter = new BufferedWriter(fileWritter); //Loading the FileWritter, so we can add content.
double offset = timestamp * (double)1000; //We need to get offset as used in osu! storyboarding, thus we multiply the value by 1000 to get milliseconds.
String data = i + "," + Double.toString(offset).split("\\.")[0] + "," + Float.toString((magnitudes[i] + 60)/3); //We convert the offset to string and split it by dot, then take values before the dot, so we have milliseconds. Then we convert magnitudes to string, in my opinion, it looked well divided by 3, so I divided it by 3.
bufferWritter.write(data); //Writes data in this format to file: "index,time in ms,volume"
bufferWritter.write("\n"); //You need each line separate, so \n is used to add a new line.
bufferWritter.close(); //Closes the file
} catch (IOException ex) {
//Well, it won't allow you to compile without it, so you need to catch IOEx, but you don't need the Logger. Logger.getLogger(ChartAudioBar.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
};
}
//We don't care about anything till next comment
public void play() {
this.startAudio();
}
@Override public void stop() {
this.stopAudio();
}
protected BarChart<String, Number> createChart() {
final CategoryAxis xAxis = new CategoryAxis();
final NumberAxis yAxis = new NumberAxis(0,50,10);
final BarChart<String,Number> bc = new BarChart<String,Number>(xAxis,yAxis);
bc.setId("barAudioDemo");
bc.setLegendVisible(false);
bc.setAnimated(false);
bc.setBarGap(0);
bc.setCategoryGap(1);
bc.setVerticalGridLinesVisible(false);
bc.setTitle("Live Audio Spectrum Data");
xAxis.setLabel("Frequency Bands");
yAxis.setLabel("Magnitudes");
yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis,null,"dB"));
XYChart.Series<String,Number> series1 = new XYChart.Series<String,Number>();
series1.setName("Data Series 1");
series1Data = new XYChart.Data[128];
String[] categories = new String[128];
for (int i=0; i<series1Data.length; i++) {
categories[i] = Integer.toString(i+1);
series1Data[i] = new XYChart.Data<String,Number>(categories[i],50);
series1.getData().add(series1Data[i]);
}
bc.getData().add(series1);
return bc;
}
//Above code is just calling start, stop functions and rendering the chart, you don't really need to know anything about it.
private void startAudio() {
//We don't need the condition here, you should remove even the ending bracket after these two commnands. if (PLAY_AUDIO) {
getAudioMediaPlayer().setAudioSpectrumListener(audioSpectrumListener);
getAudioMediaPlayer().play();
//}
}
private void stopAudio() {
if (getAudioMediaPlayer().getAudioSpectrumListener() == audioSpectrumListener) {
getAudioMediaPlayer().pause();
}
}
private static MediaPlayer getAudioMediaPlayer() {
if (audioMediaPlayer == null) {
//We need code below, but I commented it anyway to shot the difference, we need a local file, so this won't work.
//Media audioMedia = new Media(AUDIO_URI);
//So we create a local file, for example mp3.mp3 in current folder, it can be anything you want.
File audiofile = new File("mp3.mp3");
//Now wee need to make audioMedia as it was previously - So it needs to be URI, which .toURI() ensures easily, but as well as that, it needs a string URI, so .toString() will ensure this.
Media audioMedia = new Media(audiofile.toURI().toString());
audioMediaPlayer = new MediaPlayer(audioMedia);
}
return audioMediaPlayer;
}
@Override public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
play();
}
public static void main(String[] args) {
launch(args);
}
}
If you are not interested in taking a look at the Java code, you may just get executable file. Use it by putting mp3.mp3 file to folder with it and run it. Then wait till song ends. Now, why the heck would I use Ruby, not Java. First thing is, Java is less tolerant, making the code in Ruby is very flexible, you don't need to care about data types etc. it is just more comfortable for so simple applications. Second reason is that you don't need to compile anything, you have one code and it goes line by line. Doesn't require any thinking. Third reason is the fact that some people will understand the code and may make nice things with it with just little knowledge - That means you can send only data.txt to someone and he may change and run the code just so it makes what you want. Last reason is that if there is someone to do this, he may run the code through Ruby on Rails - Means that you'll be able to get your storyboard in your internet browser. Only disadvantage I can see here is that people will be too lazy to install Ruby, but same may apply for any other language apart from supported languages by default. If you have any trouble, don't worry and contact me.
If you ask me, why not everything in Ruby? I wanted to prove that anyone can do it by editing some template. Moreover, it's because I would force people to install some gems and I think most people use Windows here, so some gems could be disaster. I may later do this in other languages, for example Python, but I don't think GUI is ever coming as it's not that much customizable, so I would like to avoid it.
MoonShade - SGL Editor
Damnae - SGL Fork
Wafu - Guide
If you need any help with your code, feel free to ask me in-game or send me PM and I'll try to point out what you did wrong. Eventually, you may ask me just for check whether it won't cause big SB load. Of course, I may later describe your issue in "Optimization & Common Issues" section, so others have opportunity to get known what to be aware of.
You may also like: x264 GUI for high quality video conversion