Artificial Gensokyo -Eternal- Warcraft 3 USEast Clan for Touhou DayDream Breaker |
|
| Toho Danmakufu | |
| | Author | Message |
---|
ZeiZou
Posts : 108 Join date : 2010-06-06
| Subject: Toho Danmakufu Sun Jun 27, 2010 6:53 pm | |
| Download LinkTouhou Danmakufu (東方弾幕風, alternate spelling Danmakuhu) is a relatively easy-to-use STG maker program which leans toward making Touhou-style games with C-like programming language. Note that this program MUST be run in japanese locale in order for it to work. You can use this program to run it in japanese locale if you cannot change your system locale for whatever reasons. apploc.msi Also note that I have not tested apploc.msi on ubuntu or linux or any other os than windows. Each script file is a plain text in 7-bit ASCII code, if no Japanese characters are used. It can be edited with standard text editors such as Notepad or Notepad++. Personally I prefer to use notepad because its very straight forward. If you want to use Japanese characters, the scripts must be written in Shift-JIS Japanese code set [not UTF-8]. Such scripts can be edited with Japanese or multilingual text editors. I wouldn't recommend it unless your REALLY good with the language. Danmakufu Basics Tutorial By Blargel - Spoiler:
All programming languages (to my knowledge) require some sort of framework for everything else to be stuck in and Danmakufu is no different. The following is the framework for Danmakufu. I encourage you to go ahead and create a folder in your script directory called tutorial and paste the code below in a new .txt file so that you have something to experiment with as you follow this tutorial. If the script ever gets screwed up and you have no idea how to fix it, just clear it and copy-paste it all over again.
#TouhouDanmakufu #Title[Attack Name] #Text[Attack Description] #Image[.\img.png] #BackGround[User(.\bg.png, 1, 1)] #BGM[.\bgm.mp3] #PlayLevel[Normal] #Player[FREE] #ScriptVersion[2]
script_enemy_main { @Initialize { }
@MainLoop { }
@DrawLoop { }
@Finalize { }
@BackGround { } }
If you try running this script in Danmakufu, you'll notice that it does absolutely nothing. In fact, the boss should die as soon as it is created because there is no function setting its life value. For now, however, let's mess with some of the values in the framework to give you an idea as to what they actually do.
#TouhouDanmakufu
This tells Danmakufu that the file is a Danmakufu script. Specifically, it is telling Danmakufu that it is a single attack for a boss. If you delete this line, you'll see that the program will no longer have it on the list as a executable script, even if it's the most awesome attack in the world. However, not everything you create with this program will be a single attack. With #TouhouDanmakufu[Plural], you are telling Danmakufu that this script is a boss with multiple attacks. With #TouhouDanmakufu[Stage], you are telling it that the script is a stage. And with #TouhouDanmakufu[Player], you are telling it that the script is a playable character. For now, we will just be using #TouhouDanmakufu
#Title
Quite simply enough, this tells Danmakufu what to display as a name for this particular script when it is listed in the program as an executable script. If you put #Title[Lol], then it will be displayed as "Lol". If you put #Title[Awesome Attack of Doom], it will be displayed as "Awesome Attack of Doom". Go ahead and change the text in the brackets and check in the program to see what I'm talking about. (Be sure to save the .txt file every time you want to run the script so that Danmakufu runs the correct version. Otherwise, it will run the version from the last save and no changes will have appeared.)
#Text
This is similar to #Title. Instead of affecting the title, however, it affects the description of the script when the title is highlighted. Also, this directive is optional. You can delete this line and no description will show up.
#Image
This determines the preview image that is shown when the title is highlighted in Danmakufu. Danmakufu handles most of the common image types such as .jpg, .gif, and .png. To choose the image that you want displayed, you will need to know where the image is. For now, copy a small image into your tutorial folder you made earlier and rename it as "image.jpg" or whatever the extension is. Now, when you put #Image[.\image.jpg] (or whatever the extension is) and save the script, the image should show up when you select the script in Danmakufu. Examining the text, .\image.jpg, you may be able to tell that the period in the beginning means the directory that the script is currently in. If the image is not in the folder, but say in a folder labeled images which is in the tutorial folder, then you will need to put .\images\image.jpg in the brackets in order for Danmakufu to find the image. If the image is in the folder that the tutorial folder is in, then you use .\..\images\image.jpg where the double period means the directory that the current script's directory is located in.
This is also optional and can be deleted without affecting the executability of the script.
#BackGround
This is rarely ever used because @Background is much more flexible, but more on that later. #Background is used to either generate a tiled scrolling background with an image you provide, or a default background built into the system. First, the default backgrounds. To my knowledge there are only two, Default and IceMountain. If you replace the text in the brackets for this directive, you'll be able to see what they look like. If you would like to use an image to make a tiled background, you will need to use User(imagedirectory, x, y) where imagedirectory is the location of the image you want as explained in the last section, x is the speed at which the background will scroll from left to right, and y is the speed at which the background will scroll from top to bottom. Let's use the image that we used in #Image so assuming the image is still right next to the script file, the directory is .\image.jpg. Let's also put 1 for x and y. The directive should now look like this: #Background[User(.\image.jpg, 1, 1)]. If done correctly, when you run the script now, there should be a tile background made from the image that is scrolling diagonally. You can mess with the numbers for x and y to see how it affects the scrolling of the background and keep in mind that negatives and decimals are supported.
This is also optional and can be deleted without affecting the executability of the script.
#BGM
This tells Danmakufu what music to play. The music is selected in the same way as the images so let's bring in an mp3 into the tutorial folder. See if you can make Danmakufu run the music without me having to tell you exactly what to put in the brackets.
This is also optional and can be deleted without affecting the executability of the script.
#PlayLevel
PlayerLevel adds text to the right side of the screen. It's basically there for identifying whether or not the script will display "Lunatic" or "Easy" on the side of the screen. It's not really important, so most people leave it out, but if you plan on making several difficulties of your script, you can indicate a difficulty level with this function.
#Player
This tells the program what playable characters are allowed to be used in the script. To choose which characters are allowed, you must know where the script for the characters are. In most cases, they will be in the player folder of Danmakufu, like the Rumia playable character that is packaged with the program. To refer to that directory, you should use player\Rumia\Rumia.txt. As you may already know, there are also four playable chars already built into Danmakufu: ReimuA, ReimuB, MarisaA, and MarisaB. Obviously, there is no directories in which these are stored so instead, you should refer to them as REIMUA, REIMUB, MARISAA, and MARISAB. The caps are required. Also, you can kill two birds with one stone and refer to them as REIMU (to add both ReimuA and ReimuB to the playable characters list) and MARISA (to add both MarisaA and MarisaB to the playable characters list). Finally, if you just put FREE, all characters in the player folder as well as the four built in characters will be available for use. Here's an example where ReimuB, MarisaA, MarisaB, and Rumia is allowed: #Player[REIMUB, MARISA, player\Rumia\Rumia.txt].
This is optional and, if deleted, will default to FREE.
#ScriptVersion
I'm not sure what this does, but always have it and do not change it.
script_enemy_main
This indicates the beginning of the script that dictates the behavior of the enemy. Before anything else, you should declare variables and create the functions that you will be using most often. I'll teach you how to do that later.
@Initialize
This is where you should put stuff that should only be run once in the beginning of the script such as setting the boss's life, score, spellcard name, timer, etc. I'll teach you how to do that later.
@MainLoop
This is where you should put how you want the enemy to behave such as spawning bullets or moving around. I'll teach you how to do that later.
@DrawLoop
This is where you should put how you want things drawn. Please refer to Nuclear Cheese's Danmakufu Drawing Tutorial for more information on how to use this.
@Finalize
This is where you should put stuff that should happen only once at the end, such as dropping point items or deleting the boss's sprite.
@Background
This is where you control the behavior of the background. It replaces whatever commands you gave it in #Background if any and anything drawn in this is drawn behind anything drawn in @DrawLoop. Please refer to Nuclear Cheese's Danmakufu Drawing Tutorial for more information on how to use this.
Other notes
See all those braces (these things: "{" and "}")? Always make sure those are there. They tell the script when each section begins and ends. If you have an unequal amount or if they are placed inappropriately, Danmakufu will raise an error.
In fact, Danmakufu will probably raise an error more often than not when you are trying to create something. If you can't deal with figuring out what tiny little thing you did wrong every single time, you definitely should not be learning how to use this program. Or any programming language for that matter.
Variables and Control Statements
Variables and control statements are invaluable tools to creating anything in Danmakufu. If there were no such thing, than this program would be completely useless. [edit] Variables
Variables store information for later use. They can be used to store numbers (e.g. 14, 2398.37, -583) , strings ("Hello, world!", "2957 is a number", ":V"), and booleans (true, false). To create a variable, all you have to do is put let test; where "test" is the name of the variable. This basically translates to "Create a variable called 'test.'" Make sure the semi-colon (";") is there because it signifies the end of a statement. When creating a variable, you can also give it a value immediately on the same line: let test = 500;. This basically translates to "Create a variable called 'test' that has an initial value of 500." Again, make sure the semi-colon is there because Danmakufu is very picky and will raise an error if it doesn't know where the end of a statement is. Make sure that when creating variables, you never give two variables the same name or an error will be raised and the script will be terminated when run.
To change the value of a variable, all you have to do is put test = 501;. This will mean "Set the variable called 'test' to equal 501." You can also adjust the value of the variable using math like this: test = 14*37-199/34+13;. Also, you can adjust the value of a variable based off of it's current value. For example, if the current value of test is 20 and you put test = test + 5;, then the value of test would become 25. There is a shorter way of writing that however. test += 5; means the exact same thing as test = test + 5;. Similarly, test -= 5; is the same as test = test - 5;, test *= 5; is the same as test = test * 5;, and test /= 5; is the same as test = test / 5;.
You can also use other variables when changing the value of a different variable. For example, if there were another variable called test2 with a value of 18, then the line test = test2 + 34; would cause test to assume the value of 52 while test2 would stay as 18.
Arrays are special types of variables that store multiple values under the same name. They are created in the same way, but are defined as an array when you set them in a specific way after the equal sign. Here is a line that creates an array: let array = [4, 1, 6, 3, 5, 2];. As you can see, it is now storing six values under the same name. To call a specific value, you put the array's name, open bracket, a number, close bracket. For example, if after creating the array, I were to write a line that said let something = array[4];, the newly created variable, something, would have a value of 5 because that was the 4th value in the array (the 1st value to us is the 0th value to the computer). Changing the value in an array is also similar. array[0] = 1; would cause the array to now be [1, 1, 6, 3, 5, 2].
The size of an array is constant so after making an array, you must make sure you never call something that goes past the limit of the array. For example, array[6] = 9; would cause an error to occur because there is no 6th value to change in the array that I had created. There are a few operations that work with arrays.
Arithmetic: The 1st element is operated on with the 1st element, the 2nd with the 2nd, etc. This only works if the arrays are the same length
[2, 4, 7] + [1, 5, 2] = [3, 9, 9] [2, 4, 7] - [1, 5, 2] = [1, -1, 5] [2, 4, 7] * [1, 5, 2] = [2, 20, 14] [2, 4, 7] / [1, 5, 2] = [2, 0.8, 3.5]
You can also combine arrays or add values.
[2, 4, 7] ~ [1, 5, 2] = [2, 4, 7, 1, 5, 2] [2, 4, 7] ~ [1, 5] = [2, 4, 7, 1, 5] [2, 4, 7] ~ [1] = [2, 4, 7, 1]
Or delete values.
erase([2, 4, 7], 0) = [4, 7] erase([2, 4, 7], 1) = [2, 7] erase([2, 4, 7], 2) = [2, 4]
Control Statements
Control Statements direct the flow of script. It determines when things should happen such as when bullets should be fired, when the boss should move, or when a special effect should be created. Without these, everything would happen once per frame, or 60 times a second, regardless of any conditions.
There are many different types of control statements that help you direct the flow but before we talk about them, you will first need to know a few boolean operators, or comparisons:
==: This means "is equal to". 12==12 is true. 28==2 is false. 8==14 is false. !=: This means "is not equal to". 12!=12 is false. 28!=2 is true. 8!=14 is true. >: This means "is greater than". 12>12 is false. 28>2 is true. 8>14 is false. <: This means "is less than". 12<12 is false. 28<2 is false. 8<14 is true. >=: This means "is greater than or equal to". 12>=12 is true. 28>=2 is true. 8>=14 is false. <=: This means "is less than or equal to". 12<=12 is true. 28<=2 is false. 8<=14 is true.
&&: This means "and". If two true statements are joined with && as a single statement, then that statement is true. Otherwise, it is false. ||: This means "or". If two false statements are joined with || as a single statement, then that statement is false. Otherwise, it is true.
And now for the actual control statements. The most common one is the if statement, which is often followed up with else though it depends on what you are trying to accomplish. The if statement works the same way as the word "if" in real life. For example, the English sentence, "If you sleep, you dream" means that you will dream if and only if you are sleeping. Likewise, the Danmakufu statement, if(PowerLevel > 9000){VegetaScreams = "It's over nine thousand!";} means that the variable VegetaScreams will store the string, "It's over nine thousand!" if and only if the variable PowerLevel is greater than 9000.
Basically, the structure of an if statement is if(requirements){stuff happens}. However, it's actually much better to organize it like this:
if(requirements){ something happens; something happens; something happens; }
This way, if multiple things are happening as in this instance, it is much easier to see what is going on. Note that if the requirements are not fulfilled, then Danmakufu will skip over the stuff that happens in between the braces, which should make sense. If you put else{stuff happens} after the closing brace of an if statement, it will skip to that area instead.
if(requirements){ something happens; something happens; something happens; }else{ something else happens; something else happens; something else happens; }
So in this case, if the requirements are fulfilled, something will happen three times. If the requirements are not fulfilled, something else will happen three times. Note that in this set up, something and something else cannot happen at the same time. It is either one or the other.
There is a variation of the if statement called alternative. It is used when there are more than two outcomes you want. Here is an example:
alternative(variable) case(1){ x+=5; } case(2){ x-=5; } case(3){ x+=2; } others{ x=0; }
What this snippet of code basically means is "Look at the variable called 'variable'. If it's equal to 1, then add 5 to the variable 'x'. If it's equal to 2, then subtract 5 from the variable 'x'. If it's equal to 3, then add 2 to the variable 'x'. If it's anything else, then make the variable 'x' store the value of 0." As you can see, alternative is basically just a shorthand way of writing a looooooooooong if statement.
Another common control statement is loop. This is much easier to understand and use than if and else. Suppose you wanted something to occur multiple times, but you didn't want to take up space by writing the same thing over and over again. This is where the loop statement comes in. The structure of loop goes like this:
loop(20){ something happens; }
The number in the parentheses tell Danmakufu how many times to run what's in the braces. In this set up, something will happen 20 times. loop is also known as time. If you do not put a number in, then you will create an infinite loop and will have to end task Danmakufu unless break; is used. break immediately exits out to just past the next close braces ("}").
while statements are rather useful as well, but they may sometimes cause infinite loops that will force you to end task Danmakufu.
while(variable<10){ something happens; }
In this example, something will continue to happen until the variable variable is no longer less than 10. If it's already not less than 10, then something will not happen when the program reaches this part of the script. Note that the program will not move past this part until variable no longer is less than 10 so in this case, an infinite loop has been created. To counteract this, you can put something in the braces that changes the value of variable in a way that will eventually cause it to become greater than or equal to 10. break can also be used to exit out of the infinite loop.
Finally, there are the ascent and descent statements which are similar to loop. The difference is, they involve a variable as well that increases or decreases by 1 every loop until it reaches the limit that you set. The variable you use for this statement does not have to be created with let.
ascent(i in 0..5) { array[i] = i; }
descent(i in 0..5) { array[i] = i; }
In these two cases, the variable used is i and the numbers go from 0 to 5. Those two snippets of code are equivalent to the following two snippets of code respectively.
array[0] = 0; array[1] = 1; array[2] = 2; array[3] = 3; array[4] = 4;
array[4] = 4; array[3] = 3; array[2] = 2; array[1] = 1; array[0] = 0;
Other notes: When creating variables in control statements, something happens to where the variable can be used.
let variable1 = 0; loop(5){ let variable2 = variable1; variable2 += 10; } variable1 = variable2;
In this example, an error will be raised. Why? It has to do with the scope in which variables can be used. In the loop, variable1 is called, something perfectly fine, but once the loop is exited, variable2 is called, something that is not allowed. This is because every time Danmakufu reaches the end of the loop, variable2 ceases to exist. This also explains why 5 variables of the same name can seemingly exist (which is actually not really the case). Basically, variables in the inside cannot be accessed by things outside, but variables in the outside can be accessed by things inside.
Initializing a Boss Fight
Now, we finally get to the part where you learn how to make the boss do something. Well, sorta. You get to learn how to set a boss's life and stuff like that. But hey! That's important too!
The following script has a bunch of stuff in it's @Initialize, most of which will probably never be actually in it at the same time in an actual script and a few variables to store a couple values to make information easier to keep track of. It also has some things in the @DrawLoop and has @Background just omitted completely (@Background is actually optional) which you should learn from Nuclear Cheese's Tutorial sometime in the near future.
#TouhouDanmakufu #Title[Boss Initialize] #Text[Boss Initialization with almost everything you could possibly need stuck in @Initialize] #Player[FREE] #ScriptVersion[2]
script_enemy_main { let BossImage = "script\img\ExRumia.png"; let BossCutIn = "script\img\ExRumia.png"; @Initialize { LoadGraphic(BossImage); SetLife(100); SetDamageRate(10, 10); SetTimer(50); SetInvincibility(30); CutIn(YOUMU, "Spellcard Name", BossCutIn, 0, 0, 200, 600); SetScore(500000); SetEnemyMarker(true); SetDurableSpellCard; LastSpell; Concentration01(60); Concentration02(60); MagicCircle(false); SetEffectForZeroLife(180, 100, 1); }
@MainLoop { }
@DrawLoop { SetTexture(BossImage); DrawGraphic(GetX, GetY); }
@Finalize { } }
Quite a bit of stuff that can go into @Initialize and I just showed some of the more common ones. If you really wanted to, you could also make the boss move to a specific spot when it is spawned, shoot some bullets or lasers, or spawn familiars when it starts up. For now, let's stick with what I stuck in there right now.
LoadGraphic
This function loads the image that is referred to by the directory path in the parentheses, getting it ready to be drawn in the @DrawLoop. More about this in Nuclear Cheese's Tutorial.
SetLife
The cool thing about Danmakufu is that all the functions are named very well. If you take a look at the function name, you should be able to tell, in general, what it does. Accordingly, SetLife, quite obviously, sets the life of the boss. All you have to do is put a numerical value in the parentheses. The higher the number, the longer the boss will stay alive. If you don't set a life in an attack, the boss will automatically die when it spawns. Never set the value in the parentheses as a variable. The script will work, but you will have issues later when stringing attacks together to make a boss.
SetDamageRate
This function determines what percentage of the player's firepower actually gets converted to damage. The first number is the percentage that a shot's damage will deal while the second is the percentage that a bomb's damage will deal. For example, as it is right now, SetDamageRate(10, 10);, if a player's shot normally deals 10 damage per hit and a player's bomb normally deals 100 damage, the shot will actually only deal 1 damage to the boss and the bomb will only deal 10 damage. If this function is not called, it will default as SetDamageRate(100, 100);.
SetTimer
Another somewhat obvious one, SetTimer sets the timer in seconds for the attack to run out. If you don't know, the timer is in the top right corner right below the life bar of the boss when you run the script. If you don't set a timer, the timer will stick at 99 and the attack will not run out until the boss's life hits 0. Also, you can set the timer to be higher than 99, but the timer will display 99 until it can finally correctly display a two digit number.
SetInvincibility
This function determines how much invincibility time the boss will have in frames (1 second = 60 frames). Usually it's called in order to prevent bombs that finished off a previous attack from damaging the next life bar. If you don't set this function, it defaults to 0 frames of invincibility.
CutIn
This function displays the name of the attack as well as a cut in of the boss that you have to provide. This is the most complicated function you've seen thus far with 7 parameters to fill out. Let's go in order. The first parameter is the style in which you want Danmakufu to display the cut in. YOUMU refers to Touhou Youyoumu, Perfect Cherry Blossom, while KOUMA refers to Touhou Koumakyou, Embodiment of Scarlet Devil. At the time of this writing, those two are the only available options for the first parameter. The second parameter is the spellcard name in quotes. Something like Power Sign "Chaotic Spin" or something like that. However, you may notice that if you try to put quotes inside the quotes, it will confuse Danmakufu (and therefore raise an error). To unconfuse it (and confuse yourself), write it like this: CutIn(YOUMU, "Power Sign """Chaotic Spin"", BossCutIn, 0, 0, 200, 600);.
The last five parameters are what this function is really supposed to be for (I usually just use it for the spellcard name). The first of the last five is the directory of the image you want to use for the cut in, in this case stored by the variable BossCutIn. You should know how to point to the correct image by now. The last four determine the four sides of the image (left, top, right, and bottom in order) that you referred to in the last parameter. Generally, if your cut-in image is just the image you want as opposed to a sprite sheet, the first two numbers (left and top) will be 0, the third number (right) will be the width of the image in pixels, and the last number (bottom) will be the height.
SetScore
This function sets the base spellcard bonus that is rewarded upon a spellcard's defeat with no bombs or deaths. If the function is not called, then a bonus will not be rewarded regardless of how well the player does.
SetEnemyMarker
This function determines whether the red enemy indicator at the bottom of the screen will show up or not. It will show up if the value in the parentheses is true, and will not show up if the value is false. If the function is not called, then the enemy indicator may or may not show up depending on if SetScore was called. If SetScore was called, then the indicator will show up. Otherwise, it will not.
SetDurableSpellCard
This function, when called will cause the spellcard bonus to be collected even if the timer ran out and the player did not deplete the boss's health bar. If it isn't called, then a timed out spellcard will not give a spellcard bonus. This does not have an effect on the spellcard bonus in regard to deaths or bombs.
LastSpell
This function, when called will disable bombs for this particular attack. If it is not called, bombs will be allowed to be used. Also, when the player is hit, the attack will end immediately and the player will not lose a life.
Concentration01
This function creates a special effect over the boss that makes it look like it's charging energy. The value in the parentheses determines how long the boss will charge.
Concentration02
This function works exactly like the above function but the special effect is that of charging icy energy (i.e. snowflakes).
MagicCircle
This function determines if the rotating magic circle is drawn. If true, it will be drawn. If false, it will not be. If this function is omitted, it will default to true.
SetEffectForZeroLife
This function determines the effect that happens when the boss's life reaches 0. The first parameter determines how many frames the boss and the effect will linger. The second determines the intensity of the motion blur of everything being drawn with 255 being maximum intensity. And the last value determines how much slow down will occur as the boss dies. The higher the number, the more the slow down. 0 is no slowdown and 1 is half speed. If this function is not called, then no extra effect will occur.
Other Notes
Hopefully by now, you realize how the built in functions of Danmakufu work. There's the name of the function (which is case sensitive by the way), usually some parentheses, and some values in the parentheses that determine the specifics of the function. By now you can probably look in the Touhou wiki's Danmakufu section and understand what is meant by the list of functions. [edit] Firing and Controlling Bullets
Okay, now for the part of the tutorial that you probably wanted to get to much earlier (or skipped to without reading anything else). Anyway, first of all, make a .txt file with the following code in it:
#TouhouDanmakufu #Title[how do I shot bullets?] #Text[lol i dunno] #Player[FREE] #ScriptVersion[2]
script_enemy_main { let frame = 0; @Initialize { SetLife(1000); SetEnemyMarker(true); }
@MainLoop { SetCollisionA(GetX, GetY, 32); SetCollisionB(GetX, GetY, 24); if(frame==60){ CreateShot01(GetX, GetY, 3, GetAngleToPlayer, RED01, 10); frame = 0; } frame++; }
@DrawLoop { }
@Finalize { } }
This is a very simple script to make the boss, which now has a hitbox to be shot at, to fire a standard red bullet at the player every second. Let's examine the flow of the program as it reads the script.
First, it creates a variable called frame and stores the value of 0 in it. It then goes into @Initialize where the boss's life is set to 1000, and the enemy indicator is enabled. Finally, it goes into the @MainLoop which will continue to run once per frame. For the first 60 run-throughs, nothing will happen except the commands SetCollisionA(GetX, GetY, 32);, SetCollisionB(GetX, GetY, 24);, and frame++;. SetCollisionA sets the boss's collision to player bullets so that the player can deal damage. The first parameter is the x-coordinate of the center of the collision circle, the second the y-coordinate, and the third the radius of the collision circle. As you may have noticed, GetX and GetY were used in place of numbers for the first two parameters. These are functions that find the current x-coordinate and y-coordinate, respectively, of the boss. SetCollisionB is used the same way as SetCollisionsA, but the type of collision it detects is boss to player character collision, which would cause the player to lose a life. Finally, frame++ is equivalent to frame += 1 or frame = frame + 1. As you can see, after the first 60 runs through the @MainLoop the variable, frame, will finally equal 60 and the flow of the script will go into the if statement for the first time.
In the if statement, the function CreateShot01 is called, which is set to fire a red bullet aimed directly at the player. This is the most basic command for firing a bullet and it, along with many others, will be explained later on in this section. After firing the shot, the variable frame is set to 0 again, causing the @MainLoop to do basically nothing again for 60 frames before the flow enters if statement once again. This will continue infinitely until the player depletes the enemy's life.
Hopefully, now you know how control statements and variables will help you control what happens when and how often. With just this simple set up, the timing of all the bullets you want shot can be controlled with the frame variable and one or more control statements.
Bullets Graphics
In the CreateShot01 function that I called in the above example, the parameter RED01 tells Danmakufu to use a small red bullet for that shot. A full list of all the bullet graphics built into the Danmakufu as well as example graphics can be found in the Touhou wiki here. Go ahead and experiment with them by replacing RED01 with other values such as GREEN05 or WHITE22.
Bullet Control
Danmakufu gives you many different ways to fire bullets which vary in the amount of control you have over the bullet as well as how much processing power the bullet requires. Let's go in order of most basic and least control to most complex and most control.
CreateShot01
This is the easiest function to use. The parameters that it asks for, in order, are starting x-coordinate, starting y-coordinate, speed, angle, graphic, and delay. In the x/y coordinate plane of Danmakufu, (0, 0) is the top left corner. The greater the x coordinate, the farther right the point goes and the greater the y coordinate, the farther down the point goes. Speed is measured in pixels per frame and can be a decimal and negative. For angle, 0 is straight right while 90 is straight down. In the example above, I used GetAngleToPlayer, a function that gets the current angle from the boss to the player. Note that it does NOT get the current angle from the spawning point of the bullet to the player so the bullet will only aim at the player if it is spawned directly on top of the boss (and thus at the point (GetX, GetY)). Graphics have already been explained in the previous section. And finally delay is the amount time in frames that the bullet is postponed before being fired. Before it is fired, its position will be displayed as a glow of the same color of the bullet. For a better idea of what I mean, change the value in the example to something much greater or much lower to see a difference.
CreateShot11
This is about as easy to use as CreateShot01. The only difference is that to choose direction, it doesn't use angle and velocity as its 3rd and 4th parameters. Instead it uses two velocities, horizontal and vertical, as the 3rd and 4th parameters. A positive horizontal velocity will cause to bullet to move right while a negative horizontal velocity will cause the bullet to move left. A positive vertical velocity will cause the bullet to move downward while a negative vertical velocity will cause it to move upward.
CreateShot02
This is similar to CreateShot01 except that it has two extra parameters: acceleration and max/min velocity. The order of parameters is x-coordinate, y-coordinate, starting velocity, angle, acceleration, max/min velocity, graphics, and delay. acceleration is measured in pixels per frame per frame and can be negative or positive, decimal or integer. If the acceleration is positive and the starting velocity is already greater than the max velocity, then the bullet will automatically start moving at the max velocity. Same for if the acceleration is negative and the starting velocity is less than the min velocity.
CreateShot12
This is similar to CreateShot02 except that is has four extra parameters: x-acceleration, y-acceleration, max/min x velocity, and max/min y velocity. The order of parameters is x-coordinate, y-coordinate, starting x velocity, starting y velocity, x acceleration, y acceleration, max/min x velocity, max/min y velocity, graphics, and delay.
CreateShotA
This is much more complicated than any of the previous bullet creation functions. This one actually needs to be coupled with two to five other functions in order for it to work. The related functions are SetShotDataA, SetShotDataA_XY, SetShotKillTime, FireShot, and AddShot. First I'm going to explain what each of these functions do and then show examples on how they work in conjunction to control a bullet.
CreateShotA initializes one of these types of bullets and makes you give the bullet a number identity for you to refer to it later in the other functions. It also asks for the starting position and delay time for the bullet. For example, CreateShotA(1, GetX, GetY, 10); would create a bullet with an ID number of 1, starting at point (GetX, GetY), with a delay of 10.
SetShotDataA gives detailed instructions to the bullet on how to behave. Here, after identifying which shot you want to control via the ID number, you can control speed, angle, angular velocity, acceleration, max/min speed, and graphic. Angular velocity is a new parameter unique to CreateShotA bullets that changes the angle of the bullet every frame by the value indicated. The cool thing about these shots is that SetShotDataA also has one more parameter that determines the time after shooting that the settings for speed, angle, angular velocity, etc. actually affect the bullet. This means that if you call this function once to set the behavior at frame 0, you can call it again to set the behavior at frame 60, or 83, or any other frame after 0. There is no limit to how many times SetShotDataA can be called for one bullet either, which gives you incredible control over the bullet. For now, as a simple example, SetShotDataA(1, 0, 5, GetAngleToPlayer, 0.1, -0.08, 2, RED01); would tell the bullet with the ID number 1 to, at the time 0 frames after being shot (immediately), to go at a speed of 5 pixels per frame, straight at the player initially but turning 0.1 degrees every frame, decelerating at 0.08 pixels per frame per frame until it reaches a speed of 2 pixels per frame, with the bullet graphic of a small red bullet. If you would like to see an example with multiple SetShotDataA functions, then scroll down a bit.
SetShotDataA_XY works the same way as normal SetShotDataA except that instead of controlling angle directly, you control it with x- and y-velocities and accelerations, much like CreateShot12. For example, SetShotDataA_XY(1, 30, 1, -2.5, 0, 0.1, 1, 5, RED01); tells the bullet with ID 1 to, at 30 frames after being shot, move with a horizontal velocity of 1 to the right and with a vertical velocity of 2.5 upwards, with no horizontal acceleration and a vertical acceleration downward with a max speed of 5 pixels per frame, and with a bullet graphic of a small red bullet.
SetShotKillTime tells the bullet when to delete itself. If it isn't called, the bullet will delete itself after moving out of the playing field like a normal bullet. If it doesn't have this function called AND doesn't move off the playing field, it will never be deleted. This function is easy to use, as it only has two parameters: bullet ID and time. For example, SetShotKillTime(1, 120); tells the bullet with ID of 1 to get deleted after 120 frames.
FireShot tells the bullet to actually be fired after all the all the settings are configured. Yes, that means that you define the behavior of the bullet before it actually ever enters the playing field so all SetShotDataA functions and the SetShotKillTime function if desired must be called before FireShot is called. As an example, FireShot(1); fires the bullet with ID of 1 and all functions referring to that bullet will no longer work. In fact, now that the bullet has been fired, the ID of 1 is freed up and CreateShotA(1, GetX, GetY, 10); can be called again without raising an error.
AddShot is similar to FireShot in that the bullet it refers to must be configured before it is added and the ID is freed up after it is added. The difference is that AddShot is used to fire a shot from another CreateShotA. The shot must be added to the target bullet before that bullet is fired as well, which makes this feature somewhat confusing to some people. One way to think of this is that you're building a bullet rather than controlling it frame by frame. Anyway, for the sake of this example, pretend I had created a bullet with an ID of 2 and configured it already. AddShot(60, 1, 2, 0); would cause the bullet with ID of 1 to fire the bullet with ID of 2 from its current position 60 frames after bullet ID 1 had been fired. Note that the extra parameter is completely useless at the moment and only has use when the firing shot is actually a laser.
And now, for an example that uses all of these functions to create some relatively complicated movement:
CreateShotA(1, GetX, GetY, 10); SetShotDataA(1, 0, 5, 0, 2.2, 0, 5, RED03); SetShotDataA(1, 60, 5, 132, 0, 0.1, 8, RED03); ascent(i in 1..60){ CreateShotA(2, 0, 0, 30); SetShotDataA_XY(2, 0, rand(-1, 1), rand(-1, -4), 0, 0.1, 0, 3, RED01); AddShot(i*2, 1, 2, 0); } FireShot(1);
If you place this code in the if statement in the beginning of this section of the tutorial, you can see that the boss will fire a bubble shot that curves initially but levels off and head into a corner, leaving a trail of falling bullets along they way. Using the knowledge of everything I taught you so far, can you figure out how each part of this snippet of code corresponds to the behavior of the bullets?
Other Notes
There are such things even more complicated than ShotA bullets which are literally manipulated frame by frame and require a lot of processing power for the computer. However, objects cover such a wide range of features in Danmakufu that it deserves its own tutorial, which may or may not be written by me later on. For now, with these tools and a bit of creativity, you should be able to make good bullet patterns already. [edit] Firing and Controlling Lasers
So by now, you should know how the functions work in Danmakufu as well as what I mean by certain terms. Hopefully this will save time when I try to explain things...
* Blargel is just talking to himself.
Lasers are fired quite similarly to bullets. Except for the more complicated ones, they behave almost exactly like elongated bullets. They even use the same bullet graphics by stretching them to fit the lengths and widths that you specify. There are four types of laser available in Danmakufu: CreateLaser01, CreateLaserA, CreateLaserB, and CreateLaserC. That's right: there's only one type of laser that is relatively simple to control.
CreateLaser01
This creates a laser that shoots at a specified angle at a specified speed. Once the laser exits the screen, it is deleted. It's like the lasers in Cirno's second non-spellcard attack in Embodiment of Scarlet Devil. The parameters for it are starting x-coordinate, starting y-coordinate, speed, angle, length, width, graphic, and delay. 8 parameters total. CreateLaser01(GetX, GetY, 2, GetAngleToPlayer, 200, 16, RED01, 20); would create a laser that starts at the boss's position, moves at 2 pixels per frame at the player, is 200 pixels in length and 16 pixels in width, is drawn with the small red bullet graphic, and is delayed for 20 frames.
CreateLaserA
Now for the fun complicated lasers. This laser is the type that, when delayed, has a thin line that shows where the laser will show up. It has no speed and instead covers the whole line. Think of Keine's Last Spell in Imperishable Night for an example. If you understood how CreateShotA worked, then this should be easy to understand as well. CreateLaserA is controlled by SetLaserDataA, SetShotKillTime, FireShot, and AddShot.
CreateLaserA has 7 parameters: ID number, starting x-coordinate, starting y-coordinate, length, width, graphic, and delay. For example, if you write CreateLaserA(1, GetX, GetY, 400, 20, YELLOW01, 60); you will initialize a laser with an ID of 1, whose base is positioned at the boss's position, with a length of 400 pixels and width of 20 pixels, with bullet graphic of a small yellow bullet, and a delay of 60 frames.
SetLaserDataA also has 7 parameters: ID number, time after creation to affect the laser, firing angle, firing angular velocity, change in length, moving speed of the base, and moving angle of the base. For example, SetLaserDataA(1, 0, 0, 1, -1, 2, 180); will cause the laser with ID of 1 to, 0 frames after its creation (immediately), aim straight right but turn at 1 degree per frame, shrink by one pixel per frame, and move to the left at 2 pixels per frame. Note that the second parameter measures frames from creation instead of frames from firing. This means that if you create a laser with a delay of 60, and tell it with this function to move, it will actually start moving even while it is delayed.
The other three functions have already been explained in the previous section and stringing them all together as a whole has also already been demonstrated there too. Therefore, I'm going to move on to CreateLaserB.
CreateLaserB
This laser works almost the same as CreateLaserA, but the position of the base is controlled a little differently. The position of the base is determined by the position of the boss, so in a way, they are installed on the boss. When delayed, has a thin line that shows where the laser will show up. Think of Flandre's Laevantein in Embodiment of Scarlet Devil for an example. CreateLaserB is controlled by SetLaserDataB, SetShotKillTime, FireShot, and AddShot.
CreateLaserB has five parameters: ID number, length, width, graphic, and delay. CreateLaserB(2, 300, 32, RED01, 120); will initialize a laser with an ID of 2, a length of 300, a width of 32, a graphic of a small red bullet, and a delay of 120 frames (2 seconds).
SetLaserDataB has nine parameters: ID number, time after creation to modify laser, change in length, distance from the boss to the laser base, change in distance from the boss to the laser base, angle from the boss to the laser base, change in angle from the boss to the laser base, firing angle, and change in firing angle. As a useful example (for once), this will cause the laser to rotate around the boss in a circle with radius of 100, pointing outwards at all times: SetLaserDataB(0, 0, 0, 100, 0, 0, 2, 0, 2);. This makes the laser start pointing to the right and rotate clockwise around the boss, keeping a constant length and distance from the boss. Keep in mind that even if the boss moves, the laser will move with it to continue circling her.
Again, please check the section of ShotA bullets to learn about the other three functions.
CreateLaserC
These lasers are usually seen only in Perfect Cherry Blossom and Undefined Fantastic Object, namely against Letty Whiterock, Merlin Prismriver, or Shou Toramaru. I'm talking about those annoying curving lasers... or at least they're annoying when Merlin or Shou uses them. These lasers are very heavy on the processor so use them sparingly (i.e. don't have fifty of them out at once). However, if you've mastered controlling ShotAs and LaserAs, this will be really easy for you to use. The parameters for CreateLaserC are exactly the same as those of CreateLaserA and the parameters for SetLaserDataC are exactly the same as those of SetShotDataA. In other words, LaserCs are pretty much ShotAs with a tail following them.
I don't think I need to explain them because they are exactly the same. Just the names are different. If I'm wrong and someone does need explaining, then just PM me or contact me in some other way. I'll edit in some explanations.
AddShot with Lasers
As mentioned in the previous section about ShotA bullets, AddShot has a fourth parameter that determines how far down from the laser base the added bullet should spawn from. An example of why this might be used can be seen from Flandre's Laevantein attack, which spawns bullets at equal intervals along the bullet as it moves. Here's a snippet of code as an example of how this will be used.
CreateLaserA(0, GetX, GetY, 300, 20, YELLOW01,0); SetLaserDataA(0, 0, 0, 2, 0, 0, 0); SetShotKillTime(0, 90); ascent(i in 0..30){ ascent(j in 0..20){ CreateShotA(1, 0, 0, 10); SetShotDataA(1, 0, 0, 90, 0, 0.1, 3, YELLOW11); AddShot(i*3, 0, 1, j*15); } } FireShot(0);
If you run this part of a script in an actual script, it should create a laser that turns and spawns bullets from it at equal intervals. You'll notice, though, that though I set the bullets to shoot at 90 degrees, or straight down, they are instead being fired 90 degrees to the laser. Keep this in mind when adding bullets to lasers. Lasers can also be added to lasers with the same result.
Other Notes
Again, there are such things as object lasers but those are too advanced for a "Basic Tutorial". I'll leave it to someone else to explain them... or maybe me when I get time again.
Last edited by ZeiZou on Mon Jun 28, 2010 1:22 am; edited 5 times in total | |
| | | ZeiZou
Posts : 108 Join date : 2010-06-06
| Subject: Re: Toho Danmakufu Sun Jun 27, 2010 6:53 pm | |
| Nuclear Cheese's Drawing TutorialSample Files- Spoiler:
Section 0: Before You Try This
This tutorial assumes you have a basic familiarity with danmakufu scripting in general. If you don't understand that yet, this tutorial won't really help you. Sorry. I recommend looking at other references that are available at this site and the main Touhou wiki.
Section 1: The Basics
Okay, first off, download the sample code: [You must be registered and logged in to see this link.]
Extract it somewhere inside of your danmakufu scripts directory.
For this section, we will be looking at the script file bg1.txt
First off - if you've worked with danmakufu code before, you've probably seen some of the drawing functions. In fact, if you've made a spellcard script which has an enemy character, you've already used some of what I'm going to show here. This section introduces the simple commands to draw 2D graphics in danmakufu.
Important commands
LoadGraphic ( <the_image> ) - This function will load an image file into memory, and get it ready for danmakufu to use. This should only be called once for each image you want to load. <the_image> can either be a string indicating the path of the file, or a string variable holding that path. Important note: if danmakufu can't find the file you give it, there will not be any warning or error, and any graphic you try to draw with that image will just not appear.
SetTexture ( <the_image> ) - This will set the given image file as the currently active one. This means that it will be the one that will be drawn when you instruct danmakufu to draw, until you change it with another call to SetTexture.
SetGraphicRect ( <left-side x> , <top-side y> , <right-side x> , <bottom-side y> ) - This will set the region of the selected image file to draw. To draw all of an image file that is M pixels wide by N pixels tall, use SetGraphicRect(0, 0, M, N). The coordinates of the region selected is known as "texture coordinates."
IMPORTANT NOTE on texture coordinates:
* The left and top side coordinates are the pixel you want to start with, with (0, 0) being the upper-left corner. The right and bottom side coordinates are one past the coordinates of the last pixel in the image you want to use. o Think of it this way. The left and top coordinates give a >= (greater than or equal) limitation, while the right and bottom coordinates give a < (less than) limitation.
DrawGraphic ( <x> , <y> ) - This draws the currently selected image to the screen at the given coordinates. These coordinates are the same as the coordinates used for positioning the player, enemy, and bullets. Important note: the coordinates you pass this function give where the CENTER of the image will be drawn.
DrawText ( <text> , <x> , <y>, <size>, <alpha> ) - This will draw text on the screen. <text> can be just about any basic value, including both strings and numbers. The coordinates passed here indicate the top-left of where the text is output. <size> determines how big the font will be, by giving the height of the text. <alpha> is used for alpha-blending, which will be discussed later in Section 2.
DeleteGraphic ( <the_image> ) - This will remove an image file from danmakufu's memory. This is usually called in your script's @Finalize, to clear up the memory that was used by the graphic.
Using These Commands
To draw a graphic, the following must be done:
1) In your script's @Initialize, you must load the image file using LoadGraphic(). 2) In your script's @DrawLoop or @BackGround, you must do the following: 2a) Set the current image to the image file you want using SetTexture() 2b) Set the texture coordinates (the area of the image file to use) with SetGraphicsRect() 2c) Draw the graphic, using DrawGraphic()
To draw text, simply call the DrawText() function in either @DrawLoop or @BackGround
Important: anything that you draw is cleared the next frame, so things must be drawn every frame for them to continuously appear on the screen. This isn't much of an issue, though, since @BackGround and @DrawLoop are both run every frame anyway.
Notes
* Whenever using an image file, setting a pixel to pure black (RGB 0 0 0) will result in that pixel being transparent when drawn in danmakufu. If you want a black color, use a close RGB value (for example, RGB 1 1 1). * I might be missing something, but as far as I can tell @BackGround only works in stage scripts, not in enemy scripts. This sort-of makes sense, anyways. * As you might notice in the script file, I used a variable named iamgefile to store the path to the image I was using. This is a good idea, and makes it easier to reference graphic files generally. * DrawText() can be useful for quite a few things. When I'm having some trouble getting a script to work correctly, I sometimes use DrawText() to output some key variables to make sure they're taking on the right values. You can also use this function to make a simple menu in your script.
Section 2: Simple Effects
Here, I'll show you how to do a few simple effects to add to the rendering we did in Section 1. For this section, we will be looking at the script file bg2.txt
Tiling
To tile a graphic means to display multiple copies of it next to each other, basically. Tiling a graphic is actually really simple in danmakufu.
When passing arguments to the SetGraphicRect() command, if you set an argument outside of the range of the image file's coordinates, danmakufu will automatically wrap around the image and tile it for you!
For example, if we have an M by N image, and we want to tile it twice horizontally, we could say: SetGraphicRect(0, 0, 2*M, N); and danmakufu would automatically tile two copies of the texture horizontally.
Rotation in 2D
To rotate a graphic, we use this command:
SetGraphicAngle ( <rotation about x> , <rotation about y> , <rotation about z> );
However, since we're working in 2D graphics, we will always set <rotation about x> and <rotation about y> to zero. Set <rotation about z> to the angle you want to rotate the graphic, in degrees.
Just like the other "Set" functions, this needs to be called before you call DrawGraphic().
Scaling
Scaling, or changing the size of a graphic, is pretty simple. Just use:
SetGraphicScale ( <x-scale> , <y-scale> );
This will make the drawn graphic <x-scale> times as wide, and <y-scale> times as tall.
Just like the others, SetGraphicScale() comes before you call DrawGraphic().
Alpha Blending
Alpha blending is a technique which gives objects a transparent appearance. To use alpha blending, use the following command:
SetAlpha ( <alpha value> );
<alpha value> is a value between zero and 255, and indicates how opaque the drawn object is. At 255, it is fully solid, which at zero, it is completely see-through. Giving values in-between yields a partially see-through graphic being drawn.
Again, SetAlpha() must come before the DrawGraphic() you want it to have an affect.
The Importance of Drawing Order
When drawing things like we are doing currently, the order in which things are drawn is very important.
To make it simple, think of each object we draw as a separate layer we put on top of the previous things that have been drawn. If the new object overlaps an older one, the new one will be seen over the older one.
Danmakufu's drawing algorithms work the same way here. Every item you draw will obscure anything else that is under it (more or less depending on the current alpha value, of course).
The Updated Draw Function Order
With what we know now, here's the full way to draw a graphic:
1) In your script's @Initialize, you must load the image file using LoadGraphic() 2) In your script's @DrawLoop or @BackGround, you must do the following: 2a) Set the current image to the image file you want using SetTexture() 2b) Set the texture coordinates (the area of the image file to use) with SetGraphicsRect() 2c) Set the alpha value with SetAlpha() 2d) Set the rotation with SetGraphicAngle() 2e) Set the scaling with SetGraphicScale() 2f) Draw the graphic, using DrawGraphic()
Notes
* Of course, you do not need to set every different value for every different thing you draw. For example, in the sample code I only call SetTexture() once and then use it to draw two instances of that graphic on screen. * In a similar vein, ALWAYS set, at the beginning of @DrawLoop or @BackGround, any value you change later. You'll notice that I call SetAlpha(255); at the start of @BackGround. Normally, this would be unnecessary, since that is the default value. But, since I later call SetAlpha(160);, I need to clear that value (which is left over from the last run-through of @Background), or it will apply to the first image as well.
Section 3: Section 3: Intro to 3D
Here, we start to look at danmakufu's 3D capabilities. For this section, we will be looking at the script file bg3.txt
3D Coordinates in Danmakufu
When learning to draw in 3D, the first thing to learn is, of course, the coordinate system.
Danmakufu's coordinate system can be pretty simply described:
* the Y axis is vertical, with positive pointing upward * the X and Z axes make the horizontal plane.
The Camera Analogy
When working with 3D graphics, it helps to think of the view as a camera, looking at a certain point in space. To set up the camera for 3D drawing in danmakufu, use these functions:
SetViewTo ( <x> , <y> , <z> ) - Sets the point in space which the camera is looking at. This, as you can see, is given in simple three-space coordinates.
SetViewFrom ( <distance> , <horizontal_angle> , <vertical_angle> ) - Sets the point at which the camera itself will be positioned. This is a bit trickier than the SetViewTo() function, though.
* <distance> indicates how far away from the target point the camera will be. * <horizontal_angle> sets the angle the camera will be at relative to the target point on the horizontal plane. I usually use 90, which makes the x axis left and right on the screen, with right being positive. * <vertical_angle> sets the angle the camera will be at relative to the target point vertically. Positive values move it above the target point, looking downward.
Drawing in 3D
When it comes to actually drawing a graphic in 3D with danmakufu, we use this function:
DrawGraphic3D ( <x> , <y> , <z> );
This works much like its 2D counterpart, DrawGraphic(), except that the graphic is given a three-space coordinate.
Important Note: The graphic will be drawn as a FLAT image in three-space. You can rotate it around to change its angle (covered in Section 4). To create a 3D object, you need to draw multiple graphics at different positions and angles (seen briefly in Section 5).
The Basic 3D Drawing Order
This works much like the drawing order for 2D graphics:
1) In your script's @Initialize, you must load the image file using LoadGraphic(). 2) In your script's @DrawLoop or @BackGround, you must do the following: 2a) Set the current image to the image file you want using SetTexture() 2b) Set the texture coordinates (the area of the image file to use) with SetGraphicsRect() 2c) Set the alpha value with SetAlpha() 2d) Set the rotation with SetGraphicAngle() ... this will be covered in detail in Section 4. 2e) Set the scaling with SetGraphicScale() 2f) Draw the graphic, using DrawGraphic3D()
Notes
* If you run the sample program, you'll notice that the image is drawn upside-down. This has to do with the Y axis being up-positive in 3D, while in 2D it was generally taken as down-positive. We can fix this with what is shown in Section 4. * To be honest, I haven't tried using 3D drawing in a @DrawLoop function. It might only work in @BackGround.
Section 4: Rotation in 3D
This is where things get fun. And by 'fun', I mean please don't crawl into the fetal position and start crying; it distracts the other students. ^_^;;
For this section, we will be looking at the script file bg4.txt How Rotation Works in 3D
In 2D, we gave a single rotation value, which rotated an image around its center.
In 3D, however, its not quite that simple. To give an angle of rotation in 3D, we need to provide three angles to danmakufu. Each one of these represents rotating the image around one of the cardinal axes (X, Y, Z).
--> Be sure to run the example script for this section. It will give you insight on how the rotation in Danmakufu works.
Important Note: From my testing, it seems that Danmakufu applies the rotation in the following order:
* first it rotates about the Z axis * second about the X axis * finally about the Y axis.
Rotating in 3D
To rotate a 3D graphic in danmakufu, we use the SetGraphicAngle() function, just like we did in Section 2 to rotate 2D images. The main difference this time is that we can supply a value in all three fields, for the rotation angles around each of the axes.
Notes:
* Take some time and try to absorb how rotation in danmakufu works. It took me a while to figure this much out, so don't feel bad if you don't get it immediately at first. * Observe how each image rotates in the sample script. Especially notice the one in the lower right, which shows danmakufu applying each rotation in order.
Section 5: The 3D Depth Buffer and Fog
This section deals with a couple functions that can make things easier to render and even look a bit cooler. For this section, we will be looking at the script file bg5.txt The Depth Buffer
The depth buffer is a special thing used in 3D graphics. What it does is, when it renders a 3D graphic, it also notes at each pixel how far away from the camera the graphic is at that pixel. When a subsequent graphic is drawn at the same pixel, the renderer will only draw it if it is closer to the camera than the old graphic at that pixel.
In simpler terms, this makes sure things are drawn in front of each other correctly.
To enable the use of the depth, buffer, you need to call these two functions:
WriteZBuffer ( <enable> )
UseZBuffer ( <enable> )
For both, set <enable> to true to enable them, or false to disable them.
WriteZBuffer() enables or disables writing to the Z (depth) buffer, while UseZBuffer() enables or disables using the depth buffer test when drawing over another graphic. Generally, you'd have them both on or both off, but there are some effects that could require using them otherwise.
These functions will generally be set first thing in your drawing function.
Fog
Fog is actually a very simple effect. Basically, as things get further away from the camera, they start to fade into a pre-set color.
To set up Fog, use the following:
SetFog ( <start_distance> , <end_distance> , <r> , <g> , <b> );
<start_distance> sets the distance from the camera at which the fog will start to take effect, while <end_distance> determines the end of the fog range; anything beyond <end_distance> will be rendered purely in the fog's color.
Of course, <r> <g> <b> are the color values for the fog.
Usually you would put the SetFog() function at the beginning of the drawing function, like with the depth buffer functions.
The New 3D Draw Function Order
1) In your script's @Initialize, you must load the image file using LoadGraphic(). 2) At the beginning of your @DrawLoop or @BackGround: 2a) Enable or disable the depth buffer, with WriteZBuffer() and UseZBuffer() 2b) Set up the fog, with SetFog() 3) For each graphic you want to draw: in your script's @DrawLoop or @BackGround, you must do the following: 3a) Set the current image to the image file you want using SetTexture() 3b) Set the texture coordinates (the area of the image file to use) with SetGraphicsRect() 3c) Set the alpha value with SetAlpha() 3d) Set the rotation with SetGraphicAngle() 3e) Set the scaling with SetGraphicScale() 3f) Draw the graphic, using DrawGraphic3D()
Notes
* Unlike most "Set" functions, the fog setting is cleared each time, so you need to call SetFog() every time. * In the sample script, you have a box drawn, while the camera rotates around it. There are a couple things to notice: o First: If you look at the script, you'll notice I never draw the bottom of the box. This is because it is never seen; not drawing a part that is never seen will save some processing power. o Second: Near the top of the file, notice the variable depth_buffer_enable. If you change its value to false and then save and run the script, you'll see what happens when you don't use a depth buffer.
Wonderful☆Life's Introduction to Menus by ChaoStar - Spoiler:
This Tutorial assumes that you know the basics of Danmakufu programming, such as drawtext, and if statements.
Alright, so a menu is really easy to make- once you grasp what you want. A simple menu could be very simple, and require no work at all- while a very complex one could take much more work.
let Selection=0; @Initialize{ SetLife(1); SetTimer(240); } @MainLoop{ if(GetKeyState(VK_UP) == KEY_PUSH){Selection+=1;} if(GetKeyState(VK_DOWN) == KEY_PUSH){Selection-=1;} }
This is the basis of our Menu. If you press up, Selection goes up by one, and the opposite, if you press down. Simple enough. Alright, the next step is where things can branch off-
if(GetKeyState(VK_SHOT) == KEY_PUSH && Selection==NumberOfSelection){dostuff;}
dostuff; is usually a change in variable, like
txtalpha1=255; txtalpha2=150; txtalpha3=150; txtalpha4=150;
As you can probably guess, this is used for making text a certain Alpha. This would be a very simple menu:
@DrawLoop{ DrawText("Option1",GetCenterX,GetCenterY,15,alph1); DrawText("Option2",GetCenterX,GetCenterY-20,15,alph2); DrawText("Option3",GetCenterX,GetCenterY-60,15,alph3); DrawText("Option4",GetCenterX,GetCenterY-100,15,alph4); if(Selection==0){alph1=255; alph2=150; alph3=150; alph4=150;} if(Selection==1){alph1=150; alph2=255; alph3=150; alph4=150;} if(Selection==2){alph1=150; alph2=150; alph3=255; alph4=150;} if(Selection==3){alph1=150; alph2=150; alph3=150; alph4=255;} if(Select>3){Select=3;} if(Select<0){Select=0;} }
As you can see here, we have four options. When the variable reaches a certain point,(in this case, more than three), it resets back to zero. So we don't have to worry about the cursor breaking free. The same goes with going below zero. Now, instead of having drawtext in the Drawloop, you could alternatively use object effects, and change transparencies and colors depending on what's selected or not. Next: how to select things...
if(GetKeyState(VK_SHOT) == KEY_PUSH && Selection==1){Dostuff1;} if(GetKeyState(VK_SHOT) == KEY_PUSH && Selection==2){Dostuff2;} if(GetKeyState(VK_SHOT) == KEY_PUSH && Selection==3){Dostuff3;} if(GetKeyState(VK_SHOT) == KEY_PUSH && Selection==4){Dostuff4;}
Simple enough, right? When the shot key is pressed, do task by variable. Anyway, that concludes my tutorial of menu basics. I hope you liked it, and please, experiment more, because menus can be a very important aspect of a game. Nuclear Cheese's Effect Object Tutorial- Spoiler:
Section 0: Before You Try This
This tutorial assumes you have a basic familiarity with Danmakufu scripting in general. If you don't understand that yet, this tutorial won't really help you. Sorry. I recommend looking at the other resources available on this site and on the main Touhou Wiki.
It could help to have some familiarity with using objects. There is also some related information that may help in my drawing tutorial. Section 1: Concepts of Polygon-Based Rendering
In order to get the most out of effect objects, you'll need to understand the basics of polygon-based rendering. This is the basic technique used to draw almost all computer graphics in games these days. To put it simply, this technique is used to draw polygons onto the screen, usually with textures (images) on them. While it generally is used in 3D, effect objects are only 2D (well, not entirely ... but for now just go with it).
Note: this section does get a bit heavy with theory. You don't need to understand every last bit of it, but I think it'll help a lot when using effect objects. No Squares Allowed
The first, and arguably the most important, concept to understand about polygon rendering is that everything is made of triangles. Now, you might be thinking "But I've seen many squares, octagons, etc ... in game X ... those aren't triangles!" But, in actuality, they are.
For instance, take a square. Now, draw a line from one corner to the opposite corner. Now you have two triangles; this is exactly how computers do it.
Games create complex shapes using nothing but triangles positioned around each other in various ways.
The main reason that triangles are used is that they only have three vertices (corners. Note: 'vertices' is the plural of vertex); this is important, because it guarantees that they will all be on the same plane and, thus, any combination of three vertices will always give a flat triangle. Texture Mapping
Texture mapping is the technique used to draw images on the triangles.
Each vertex of a triangle, in addition to its other data, is given a set of texture coordinates. Texture coordinates give the point on the texture (image) to draw onto the triangle that the vertex is at. So, if a vertex is given the texture coordinates of (0, 0), that vertex will have the upper-left corner of the image at that point.
The texture is drawn onto the triangle such that all of the vertices have their texture coordinates lined up with the texture on the triangle. Depending on how you set the texture coordinates, this can lead to either a very normal-looking image or some really interesting effects. Section 2: Using Effect Objects
Here's the section you all probably just skipped down to; this section deals with the actual commands to use effect objects. Basics
First, you need to create an effect object. If you've dealt with objects before, then it should be a piece of cake:
let objid = Create_Object(OBJ_EFFECT);
Of course, objid is a variable that holds the effect object's identifier so we can work with it later.
Now, you can set the position and such like you do with other objects. The next important function to know is how to give the object a texture:
ObjEffect_SetTexture(objid, imagefile);
This gives the effect object the texture from the file name in the variable imagefile. Remember, you must call LoadGraphic first so that the graphic is loaded into memory. You can see more on that in my drawing tutorial. Primitive Type
Next you want to assign a "Primitive Type" to the object. This is a pretty important feature; it determines how the list of vertices you will later give the object will be interpreted. There are three types:
* PRIMITIVE_TRIANGLELIST - The simplest of the three. Every three vertices you give the object are considered a separate triangle. * PRIMITIVE_TRIANGLESTRIP - This creates a strip of connected triangles. The first triangle uses the first three vertices. Every triangle after that uses the last two vertices from the previous triangle, as well as the next one. * PRIMITIVE_TRIANGLEFAN - This creates a fan of triangles. The first vertex is used for every triangle. The second and third vertex are used for the first triangle, the third and fourth for the second triangle, and so on.
A triangle list is the easiest to work with, and the most flexible, but it comes at the cost of needing to store most vertices. Vertices
Once you have selected the primitive type, it is time to add in the vertices. First, you need to decide how many vertices you want for your object. If it is a triangle list, you will need a multiple of 3 vertices; for the other two primitive types, you can have any number of vertices that is at least 3.
ObjEffect_CreateVertex(objid, 3);
This creates some space for the effect object to hold its list of vertices (in this case, it creates space for 3 vertices). You only need to do this once for each effect object.
Next, you need to set the information for each vertex. There are multiple functions that handle different parts of this. There are two that are the most important, though. The first is the one that sets the position of the vertex:
ObjEffect_SetVertexXY(objid, 0, x, y);
This example sets vertex number zero (the first vertex - remember that the vertex list starts at zero) to the position (x, y). Note that this position is relative to the effect object's own position, so if the effect object is at (100, 150) and the vertex is given coordinates of (-25, 25), then the vertex will appear on the screen at (75, 175).
Next, we want to set a texture coordinate for the vertex. This allows us to define what part of the image will be showing:
ObjEffect_SetVertexUV(objid, 0, u, v);
This sets the texture coordinate of vertex zero to the coordinates (u, v) in the effect object's image: u is the x coordinate in the image, and v is the y coordinate. (0, 0) is the upper-left corner of the image. Additionally, if you go beyond the limits of the image, the image will repeat on an infinite loop. A sample
This is a quick sample of the basics I've shown above. In order to run this, you'll need an image called bg1.png in the same directory as the script file (I used the same image as from the drawing tutorial).
#TouhouDanmakufu[Stage] #Title[Effect Object Test] #Text[by Nuclear Cheese] #Player[FREE] #ScriptVersion[2] script_stage_main { // Declare the image file name as a variable. // This makes it easier to reference it in code. let imagefile = GetCurrentScriptDirectory ~ "bg1.png"; let objid; let count = 60; @Initialize { // Load the graphic file into memory. LoadGraphic(imagefile); } @MainLoop { count--; if (count == 0) { // Create the object, and give it X and Y coordinates objid = Obj_Create(OBJ_EFFECT); Obj_SetX(objid, GetCenterX()); Obj_SetY(objid, GetCenterY()); // Set the object's texture ObjEffect_SetTexture(objid, imagefile); // Set the primitive type (here, we use a simple triangle list) ObjEffect_SetPrimitiveType(objid, PRIMITIVE_TRIANGLELIST); // Create the vertex list (3 vertices - just one triangle) ObjEffect_CreateVertex(objid, 3); // Set the coordinates and texture coordinates for the first vertex (number 0) ObjEffect_SetVertexXY(objid, 0, -20, -20); ObjEffect_SetVertexUV(objid, 0, 0, 0); // Second vertex (number 1) ObjEffect_SetVertexXY(objid, 1, 20, 0); ObjEffect_SetVertexUV(objid, 1, 255, 127); // Third vertex (number 2) ObjEffect_SetVertexXY(objid, 2, -20, 20); ObjEffect_SetVertexUV(objid, 2, 127, 255); } } @DrawLoop { } @BackGround { } @Finalize { // clear the loaded image file from memory DeleteGraphic(imagefile); } }
This script creates the effect object after one second, and places it in the center of the screen. The texture will look a bit warped, since that's how the texture coordinates are assigned. No Effect Objects in @Initialize?
There is a reason I wait to create the effect object. When I was first playing around with effect objects, I tried to create one in @Initialize. But every time Danmakufu would crash.
It turns out that Danmakufu can't handle object creation in @Initialize. Section 3: Other Tricks
There are some other things you can do with effect objects. Scaling
By using the command
ObjEffect_SetScale(objid, sx, sy);
You can scale the effect object's size by the factors sx and sy in the x and y directions, respectively. For example, if you put 2 for sx and 3 for sy, the result would be that your effect object would be twice as wide and three times as tall. Rotation
The command for rotation is:
ObjEffect_SetAngle(objid, rx, ry, rz);
This will rotate the effect object in three dimensions ... this is the only way (that I've seen, at least) to have an effect object work in 3D. For more information on how rotation works, please see my drawing tutorial. Color
You can also set a color on a vertex by using the command
ObjEffect_SetVertexColor(objid, vertex, alpha, red, green, blue);
The red, green, and blue values indicate the color to use, with each value going from 0 to 255. alpha indicates opacity, with 255 being fully opaque and 0 being fully transparent.
The color you set on a vertex will be blended with the texture around that vertex, trailing off as it goes closer to other vertices.
Last edited by ZeiZou on Sun Jun 27, 2010 7:06 pm; edited 2 times in total | |
| | | ZeiZou
Posts : 108 Join date : 2010-06-06
| Subject: Re: Toho Danmakufu Sun Jun 27, 2010 7:01 pm | |
| Danmakufu Intermediate Tutorial By Iryan and Naut - Spoiler:
This tutorial assumes you've read and understand Blargel's Basic Tutorial, and have attempted to make patterns with it. If you haven't: what are you waiting for? Get to it!
In this tutorial, the following, among other things, will be explained:
- How to create and spawn bullets in and around a uniform circle.
- How to move the boss around the field.
- How to play sound effects and music files.
- How to use create and use tasks.
- How to create indestructable bullets that react to their environment.
- How to make familiars.
- How to string together scripts to make a mini-boss.
- How to make a stage script.
- How to make an event script (when two characters talk to eachother).
This tutorial is mainly an index of various helpful posts around the MotK forum, but all of this information is very useful for the intermediate Danmakufu coder.
Consider this a group effort between Iryan and Naut.
Intermediate Bullet Patterns!
Since you've read Blargel's Basic tutorial, you should be familiar with all the CreateShot functions, as well as loop(){}. The latter you will likely be using often, as you can spawn a myriad of patterns by repeating a function while augmenting a value. Don't worry -- I'll show you what I mean. Note that anything after and on the same line as "//" in Danmakufu scripts is a comment, and will not be read by Danmakufu. Be sure to comment your work often so you don't forget what you're doing.
#TouhouDanmakufu #Title[Spawning Bullets in a Circle] #Text[Using loop to spawn bullets in a circular pattern.] #Player[FREE] #ScriptVersion[2]
script_enemy_main{
let imgExRumia="script\ExRumia\img\ExRumia.png"; let frame = 0; let angle = 0; @Initialize{ SetLife(1500); SetTimer(60); SetInvincibility(30); LoadGraphic(imgExRumia); //Load the boss graphic. SetMovePosition02(GetCenterX, GetCenterY - 100, 120); //Move the boss to the top center of the screen. } @MainLoop{ SetCollisionA(GetX, GetY, 32); SetCollisionB(GetX, GetY, 16); frame++; if(frame==120){ loop(36){ CreateShot01(GetX, GetY, 3, angle, BLUE12, 0); angle += 360/36; } angle = 0; frame = 60; } }
@DrawLoop{ //All this foolishness pertains to drawing the boss. Ignore everything in @Drawloop unless you have read and understand Nuclear Cheese's Drawing Tutorial. SetColor(255,255,255); SetRenderState(ALPHA); SetTexture(imgExRumia); SetGraphicRect(64,1,127,64); DrawGraphic(GetX,GetY); } @Finalize { DeleteGraphic(imgExRumia); } }
Begin by creating a text file with the former code in it, and run it. The main thing you should be looking at is in @Mainloop: the if(){} statement. You'll notice we loop CreateShot01 and angle. What loop does is it repeats everything inside it's braces however many times you've defined it to, in the same frame. In this case, we told Danmakufu to repeat everything inside loop 36 times, which means it will spawn 36 bullets on the boss' position, at an angle of angle, in the same frame. At the start of the script, we defined angle as 0, so all the bullets should fire at 0 degrees, or to the right. But if you run this script, you'll see that they form a perfect circle. Why? Well, that's because we set angle to be angle += 360/36 everytime the loop is run, so angle will hold a different value (in this case, 10 more than it used to) everytime we create a bullet. What this does is creates a perfect circle, because every bullet created will fire at ten degrees more than the last one, which will total to be 360 degrees because the loop is run 36 times (36x10=360).
The reason I put angle += 360/36; instead of angle += 10; was to show you that for any number that you loop(num), the angle you'll want to increment by in your loop will be angle += 360/num;. So if you want 24 bullets in your circle, the code would be:
if(frame==120){ loop(24){ CreateShot01(GetX, GetY, 3, angle, BLUE12, 0); angle+=360/24; } angle = 0; frame = 60; }
We reset the value of angle after the loop to make sure it still holds a value that we can work with later in the script, but we don't really need to do this. Sometimes purposefully dividing by a different value than what was looped can create an interesting pattern, and you might not want angle to be reset afterwards.
A simple way to change this code would be to have the angular (4th) parameter of CreateShot01 be angle + GetAngleToPlayer, which would force one particle to always be shot towards the player.
Alright, you now know how to spawn bullets in a circle, and how to increment values in loop structures to create circular patterns. But what if you want the bullets to be spawned some distance away from the boss, but still in a circle? For that, we'll use the trigonometric functions sin and cos.
The sin of an angle is the y-coordinate of a point along a circle with a radius of one. The cos of an angle is the x-coordinate of a point along a circle with a radius of one.
Diagram of Sin and Cos
Where L = the radius of the circle, A = the angle from the origin, X = cos(A) and Y = sin(A), which form the point (cos(A), sin(A)).
Note the reversed y-coordinates, remember that Danmakufu has the positive y direction pointing downward.
So, if we want something to spawn at the point (cos60, sin60), where the boss is the origin of the circle, we say:
CreateShot01(GetX + cos(60), GetY + sin(60), 3, 60, BLUE12, 10);
But this code won't really do anything, because we're only spawning it a distance (radius) of 1 pixel away from the boss, which we won't see. So to spawn it with a distance of say, 100 pixels away from the boss, the code would be:
CreateShot01(GetX + 100*cos(60), GetY + 100*sin(60), 60, BLUE12, 10);
Now we'll see some effects. A distance of 100 pixels away from the boss is pretty noticable. Just multiply sin and cos by a value, which will be your radius, to spawn particles that distance away from your origin, which in this case is the boss. So, to combine and summarize everything so far, I give you the following to place in the beginning script:
if(frame==120){ loop(36){ CreateShot01(GetX + 60*cos(angle), GetY + 60*sin(angle), 3, angle, BLUE12, 12); angle += 360/36; } angle += 4; frame = 112; }
This code will continuously spawn bullets in a circle around the boss with a radius of 60 pixels. I set angle to be angle += 4; outside the loop to make the circle shift it's position 4 degrees everytime we spawn it, so it looks like the circle is spinning.
The format for spawning bullets in a uniform circle is: [x, y] -> [originX + radius*cos(angle), originY + radius*sin(angle)]
Keep in mind that you don't just have to increment angle in a loop structure. You can increment any other variable, like radius, as well. Incrementing radius will make the circle look like it is expanding or contracting, or have other interesting effects if included inside the actual loop{}.
Using only the trigonometric functions, as well as our knowledge of loop structures and incrementing values, we can make some pretty nice patterns. Here's an example script using everything we've learned so far. Can you tell what's happening?
#TouhouDanmakufu #Title[Border of Wave and Tutorial] #Text[How to use incrementing values and loop structures to create complex danmaku.] #Player[FREE] #ScriptVersion[2]
script_enemy_main{
let imgExRumia="script\ExRumia\img\ExRumia.png"; let frame = 0; let frame2 = 0; let angle = 0; let angleAcc = 0; let radius = 0; @Initialize{ SetLife(4000); SetTimer(60); SetInvincibility(30); LoadGraphic(imgExRumia); SetMovePosition02(GetCenterX, GetCenterY - 120, 120); } @MainLoop{ SetCollisionA(GetX, GetY, 32); SetCollisionB(GetX, GetY, 16); frame++; frame2++; if(frame==120){ loop(6){ CreateShot01(GetX + radius*cos(angle), GetY + radius*sin(angle), 3, angle, BLUE12, 12); angle += 360/6; } angle += angleAcc; angleAcc += 0.1; frame = 119; } if(frame2>=-140 && frame2 <=110){ radius++; } if(frame2>=111 && frame2 <= 360){ radius--; } if(frame2==360){ frame2=-141; } }
@DrawLoop{ SetColor(255,255,255); SetRenderState(ALPHA); SetTexture(imgExRumia); SetGraphicRect(64,1,127,64); DrawGraphic(GetX,GetY); } @Finalize { DeleteGraphic(imgExRumia); } }
In this code, we have bullets spawn at six points around a uniform circle with a radius radius, which is being controlled by a second frame counter frame2. If frame2 is between -140 and 110, then the radius will increase, if it is between 111 and 360, the radius will decrease. The angle these bullets is being fired off at is always being increased by angleAcc, which is also increasing, so the circle will spin at an increasingly faster rate. Combining the oscillation of radius and the ever increasing rate of angle creates a pretty complex pattern, don'tcha think? Feel free to experiment by changing values, like changing angleAcc+=0.2 or radius+=0.5.
How to make the boss move!
I'll bet many of you have been waiting for this for quite a while... Well, it's incredibly easy and requires little explanation, as it's just like firing a bullet. Here are some boss movement functions for you to play around with:
SetMovePosition01(X-coordinate, Y-coordinate, velocity); Tells Danmakufu to make the boss start moving towards the X and Y coordinates specified, with the velocity you declare. Typically we use SetMovePosition03 instead of this one, because this one isn't very fluid.
SetMovePosition02(X-coordinate, Y-coordinate, frames); Tells Danmakufu to make the boss start moving towards the X and Y coordinates specified, taking "frames" long to get there. 60 frames is one second, 120 frames is two seconds, etc.
SetMovePosition03(X-coordinate, Y-coordinate, weight, velocity); Tells Danmakufu to make the boss start moving towards the X and Y coordinates specified, with the velocity you declared. This is a special movement function which includes "weight", which tells Danmakufu to make the boss decelerate as it's reaching it's goal, making a more fluid motion. A higher weight value will make the deceleration more sudden.
SetMovePositionRandom01(X-distance, Y-distance, velocity, left-boundry, top-boundry, right-boundry, lower-boundry); Tells Danmakufu to make the boss move around randomly inside a rectangle that you declare. The boss will move the X and Y distance you tell it to, with the velocity you declare, within the rectangle you designate by declaring it's left-most coordinate, upper-most coordinate, right-most coordinate and lower-most coordinate.
SetMovePositionHermite is complex as all blazes, so I'mma let Stuffman explain this:
Quote Stuffman:
SetMovePositionHermite(x,y,distance1,angle1,distance2,angle2,frames); x/y: This is the destination point of the boss, obviously. distance1: If you were to draw a line between the starting point and destination point, this value is how far the boss moves from that line. I'm not quite sure how this is calculated but as a rule of thumb, three times the length of the line gives you a circular movement arc. angle1: This is the angle you are moving at when you begin the movement. distance2: Same as above but for the second half of the movement. angle2: This is the angle you are moving at when you finish the movement. frames: This is how long it takes to complete the movement. In other words it sets the speed, just like SetMovePosition02.
As a sample from one of my spinner enemies in PoSR: SetMovePositionHermite(GetX()+100,GetY(),300,90,300,270,40); The enemy comes onto the screen straight down and does a 180 to its right. The helmite part of its movement involves moving 100 pixels to the right, starting at 90 degrees (down) and ending at 270 degrees (up). The distance in both cases is 300 (3*100 pixel distance) for a round movement arc. It takes 40 frames to complete this movement.
That should be enough for practical use, but you can get some really wacky movement if you play around with it. It's actually rather easy to use once you understand the arguments. The distance arguments are the only ones I don't fully understand.
You would use these functions just like calling a bullet in @MainLoop:
frame++;
if(frame==120){ SetMovePosition03(GetCenterX, GetCenterY, 10, 3); }
Tells the boss, at frame 120, to move towards the center of the playing field with a moderate velocity and decelerate as it's reaching its goal.
How to add sound effects and music!
For now, your scripts may be fun to play and/or look pretty. However, an easy way to enrich your scripts is to have shots, movements, pattern changes and other happenings be accompanied by sound effects and even controllable music files. Music and sound effects are fairly simple, just play them the same frame you want the sound effect to be heard.
However, you'll need to load it first, preferably in @Initialize, but you can load it anywhere before it's played. To load a sound effect, you simply type:
LoadSE(sound file);
And play it using the function:
PlaySE(sound file);
So in the middle of a script, it could look something like this:
script_enemy_main{
let shot = GetCurrentScriptDirectory~"shot.wav"; let frame = 0;
@Initialize{ LoadSE(shot); }
@MainLoop{ frame++;
if(frame==60){ CreateShot01(GetX, GetY, 3, GetAngleToPlayer, RED01, 0); PlaySE(shot); frame = 0; }
}
@Finalize{ DeleteSE(shot); } }
Basically, this will play the shot sound the same frame that the RED01 is fired and thus make it sound as if the shot actually has a sound effect. Something notable is that if you shoot more than one shot a frame, you need only play the shot sound once, as otherwise it will waste your computer's resources and possibly cause people to FPS spike. However, if you fire two shots on different frames, you'll want to play the shot sound on each frame. There is no reason to play the same shot sound more than once per frame. Don't do it.
You may have noticed the function DeleteSE in @Finalize inside the previous script. This is another sound function, this unloads the sound effect from memory to free up space for other loadable things for the next spellcard or script. Basically, if you loaded something in that script, you'll want to unload it in @Finalize. There is an exception to this, in stage scripts particularly, but I'll go over that in a bit.
A few other notable sound functions, this time for music files. You may be wondering, "I already know how to play music using #BGM, why do these function exist?" Well, what if you wanted to play certain music at a certain time, instead of autoplaying at the beginning of your script and automatically looping? Well, we've got functions for you:
PlayMusic(music file);
LoadMusic(music file);
DeleteMusic(music file);
These all do the exact same thing as their SE counterparts, except for larger files. This way, you can control when the music starts and stops playing in any script. Particularly useful for stage scripts.
Alright, as I mentioned before, there is an exception to the "anything loaded you want to unload in the same script" rule. Well, sort of. For spawning enemies or familiars in stage scripts or spell cards, sometimes you'll want the sound effect to play when they shoot bullets. For these scripts, it is not necessary to load or delete the sound effect before and after you use it, since you've likely loaded it in the other script (it will just find the memory slot and pull it from there). As a matter of fact, if you delete a sound effect in an enemy script (when the enemy dies), you'll no longer be able to use that sound until you load it again, which means the sound effect will just not play whenever you call it -- until LoadSE appears again. Not related to sound effects, but it should be noted that this same thing occurs for graphics too. If you load a graphic and unload it in an enemy script, then the graphic will stop being displayed for all enemies until you load it again. Just something to be aware of.
How to use Tasks!
With the CreateShotA command, you can already program rather complex bullet movements. However, what if you want to create indestructable bullets? What if you want your bullet to react to stuff that happens after you fired it, for example, to bounce off of the border of the playing field? That is when you will need object bullets.
To program an object bullet, you have to create a task.
Tasks are basically smaller scripts that run alongside the regular @MainLoop. The task script is usually inserted after the @Finalize loop before the closing bracket of script_enemy_main and can later be called like a regular function. To script a task, the following code is used:
task NameOfTheTask(put, variables, here) { stuff happens; }
So far, it looks much like a script for a regular function, and it pretty much is. To really make it run besides your main loop, you have to include a while statement. That way, the task works as long as this condition is met. Example:
task Testtask(a, b, c) { let timer=60; while(timer>0) { stuff happens; timer--; } }
For a duration of 60 frames, every frame stuff will happen. After that, the "while" condition is no longer met, and the task expires.
But, because tasks run separate from the @MainLoop, there is a very important command:
yield;
The yield command says the program to stop the task it is working on and to look if there is other code to perform. This includes the @MainLoop. That means you have to insert the yield; command at the end of the @MainLoop and inside the recurring part of your task. Otherwise, Danmakufu will proceed to ignore everything else and keep focused on the @MainLoop (or the task). That means the above code is actually wrong as it will do the "stuff" 60 times, but all during the same frame. The corrected version of that code would be:
task Testtask(a, b, c) { let timer=60; while(timer>0) { stuff happens; timer--; yield; } }
The yield command can also be called repeatedly to suspend the task. For each yield inserted, Danmakufu will run the @MainLoop once before the stuff after the yields proceeds. This means that by inserting the code:
loop(n){ yield; }
into the task, the following code will suspend for n frames.
Tasks performed by Danmakufu are independant from each other. This means that you can call the task multiple times while it is still running. These don't affect each other and are performed side by side. It also means that, like with regular loops, variables and arrays created in a task don't exist outside the task.
So, now that you know something about tasks, how does one make object bullets?
First, you have to assign an ID for your bullet. As the ID is assigned inside the task and thus doesn't exist outside of it, you can practically use anything. The most common way is to call it "obj". You assign the ID like this:
let obj=Obj_Create(OBJ_SHOT);
This creates an object that is classified as an object bullet and can be referred to by the name obj. It has, however, no properties as of now - they all have to be assigned with a separate command. The most important are:
Obj_SetPosition(obj, x coordinate, y coordinate); Obj_SetAngle(obj, angle); Obj_SetSpeed(obj, speed); ObjShot_SetGraphic(obj, graphic); ObjShot_SetDelay (obj, delay); ObjShot_SetBombResist (obj, true or false);
In order, they set the position, angle, speed, bullet graphic, delay before changing from vapor to damaging bullets, and whether the bullet is immune to bullets and death explosions (true) or can be bombed like a regular bullet (false). These commands should be called right after the declaration of your object bullet. The can, however, come in handy to regularly change the property of your bullet. For example, there is no simple command to give the bullet a specified angular velocity. To give it one, you have to call Obj_SetAngle every loop.
Speaking of which, how do you define your looping action for a bullet? Aside from some advanced shenanigans, the looping part should be called every frame, as long as the bullet still exists, right? In Danmakufu terms, this is expressed as follows:
while(Obj_BeDeleted(obj)==false) { stuff happens; yield; }
The stuff happens once every frame, as long as the statement "the object has been deleted" is a false statement. Or, to put it easier, it happens once every frame as long as the bullet still exists.
So now we have the basic structure to create an object bullet:
task Bullet(x, y, v, angle) { let obj=Obj_Create(OBJ_SHOT);
Obj_SetPosition(obj, x, y); Obj_SetAngle(obj, angle); Obj_SetSpeed(obj, v); ObjShot_SetGraphic(obj, RED01); ObjShot_SetDelay (obj, 0); ObjShot_SetBombResist (obj, true);
while(Obj_BeDeleted(obj)==false) { yield; } }
Put this after (not inside) @Finalize, and you can use Bullet(x, y, v, angle); just like a regular function to create a normal indestructable bullet.
Now, let's say we want the bullet to react to something. For the bullet to react to something, we must be able to check whether or not the conditions of reaction are given. For this, we have the information gathering commands, such as:
Obj_GetX(obj); Obj_GetY(obj); Obj_GetAngle(obj); Obj_GetSpeed(obj);
I'm pretty sure you can guess what these do!
Now, let's say that we want a bullet that reflects from the left and right border of the playing field. To do this, we specify in the while part that, if the bullet is too far on the left or right, it's angle shall be adjusted like with a real light reflection - incoming angle equals leaving angle. Because of how angles are coded, this requires some trickery:
task Bullet(x, y, v, angle) { let obj=Obj_Create(OBJ_SHOT);
Obj_SetPosition(obj, x, y); Obj_SetAngle(obj, angle); Obj_SetSpeed(obj, v); ObjShot_SetGraphic(obj, RED01); ObjShot_SetDelay (obj, 0); ObjShot_SetBombResist (obj, true);
while(Obj_BeDeleted(obj)==false) {
if(Obj_GetX(obj)<GetClipMinX) { Obj_SetAngle(obj, 180 - Obj_GetAngle(obj) ); Obj_SetX(obj, Obj_GetX(obj) + 0.1); }
if(Obj_GetX(obj)>GetClipMaxX) { Obj_SetAngle(obj, 180 - Obj_GetAngle(obj) ); Obj_SetX(obj, Obj_GetX(obj) - 0.1); }
yield; } }
This code creates an indestructable bullet that checks each frame whether it is right or left from the actual playing field. If that is the case, it will adjust the angle to mimic a real reflection, then move the bullet a little towards the center of the field to prevent it from rapid bouncing along the border.
/\ 270° <---- 180° 0° ---> \/ 90°
I just noticed that I lack the proper English vocabulary to describe why the reflected angle is calculated as it is.... It would be nice if someone else could elaborate. You know, someone who has a great deal of knowledge about angles and Danmakufu and who also likes to explain stuff to others. lol nobody -Naut
Nevertheless, now you can fire your very own indestructable-bouncing-of-the-sides-bullets, just with a little Bullet(x, y, v, angle);!
Just don't forget to insert a yield command in your @MainLoop, or the bullet won't bounce! How to use Tasks II: "Great Whirlwind"
Spellcard Picture
So, you are familiar with this spellcard and maybe even captured it. But how does it work? Well, each seperate cyclone is a cluster of several bullets with the same starting point and angular velocity, but with different starting angles and speeds. The same angular velocity ensures that every bullet of the cluster will return to the center at the same time, then the cluster will unfold again, dependent on the different angles and speeds of the individual bullets. So far, this would be easy to replicate with a CreateShotA, right? There is a little problem, however: the cyclones move downward, and still, they manage to stick to their circling motion. That is quite tougher to program.
There are actually two ways to do this; one involves CreateShotA_XY and trigonometric functions, the other involves object bullets, but since I'm here to showcase how to have fun with object bullets, we'll use these.
Let's start out with the basic framework from the previous post, with a different bullet sprite:
task Bullet(x, y, v, angle) { let obj=Obj_Create(OBJ_SHOT);
Obj_SetPosition(obj, x, y); Obj_SetAngle(obj, angle); Obj_SetSpeed(obj, v); ObjShot_SetGraphic(obj, BLUE21); ObjShot_SetDelay (obj, 0); ObjShot_SetBombResist (obj, true);
while(Obj_BeDeleted(obj)==false) { yield; } }
We said that the bullets have an angular velocity, so we'll work in a command that adjusts the angle of the bullet by a fixed amount every frame:
Obj_SetAngle(obj, Obj_GetAngle(obj) + 2);
Now we can create an object bullet that will move in a circle. Firing a ring of these bullets will get you a pulsating circle with a radius of 360*(velocity of the bullets) / (2*Pi) Using an angular veloctiy of 2 means that the bullet will have moved a full circle after 360/2 frames. That is exactly three seconds after being fired.
The bullets still won't move downwards, though. To accomplish that, we have to add a command that moves the bullet downward every frame, independently from the regular movement direction and speed. To simulate this movement, we can simply set the position of the bullet to a place the has a distance of, say, one pixel, every frame. A command that does this looks like this:
Obj_SetPosition(Obj_GetX(obj), Obj_GetY(obj) + 1);
The bullet is moved straight down by one pixel every frame. Of course, if we wanted, we could make the whole cyclone move in a specific pattern itself, like waving to left and right while moving to the bottom of the screen, adjusting the x coordinate every second dependent on the value of a trigonometric function. You can do that as practice, if you like.
So, inserting both of these commands into the bullet task, we get:
task Bullet(x, y, v, angle) { let obj=Obj_Create(OBJ_SHOT);
Obj_SetPosition(obj, x, y); Obj_SetAngle(obj, angle); Obj_SetSpeed(obj, v); ObjShot_SetGraphic(obj, BLUE21); ObjShot_SetDelay (obj, 0); ObjShot_SetBombResist (obj, true);
while(Obj_BeDeleted(obj)==false) { Obj_SetAngle(obj, Obj_GetAngle(obj) + 2); Obj_SetPosition(obj, Obj_GetX(obj), Obj_GetY(obj) + 1); yield; } }
This task creates a single bullet that spirals down the screen. To craft a cyclone, you have to fire a ring of these bullets. Several rings with different bullet speeds, actually. For this, we can define a function:
function cyclone(x, y){ ascent(i in 0..10){ ascent(k in 0..6){ Bullet(x, y, 0.5*(1+k), i*40); } }
This will fire 6 rings of 9 stormbullets each. The rings have an individual bullet speed of 0.5, 1, 1.5, 2, 2.5 and 3, and thus different radii. Calling this function for a specified place will call down a whirlwind that folds and unfolds indefinitely as it carves it's way to the bottom of the screen.
Now, to emulate the spellcard, define the task after @Finalize, define the function in the mainscript and then call the function with your main loop in regular intervals. Again, do not forget the yield; in @MainLoop!
How to make Familiars!
Best way to make familiars: Objects. Objects are made using tasks, which you should be familiar with after reading Iryan's task portion of this tutorial. The main difficulty you'll have with objects is getting the damn image to appear correctly, which can be accomplished by following Nuclear Cheese's Object Effect Drawing Tutorial. Or, if you prefer, you could skim through Stuffman's Player Script Tutorial, which explains how to make Sakuya's options, which are basically familiars. Your best bet is to go through the player tutorial though, since it gives a basic outline of how to shoot with them as well (just remember you're not using player shots).
The second way is the way I mostly use, and probably the easiest (though slightly messy). I traditionally make familiars using seperate enemy scripts, which are very easy to make and summon.
Just make a text file, and fill it with:
script_enemy_main { @Initialize{ SetLife(10); etc. } @MainLoop{ [include no collion A or B so it can't be shot or collided with, include it if you want it to be shot] [attack functions, blah blah, like you would any script] } @DrawLoop{ [draw your familiar as you would any boss] } @Finalize{ } }
And to spawn it, anywhere in your main script you write: CreateEnemyFromFile(script path of your familiar/enemy, x-coordinate, y-coordinate, velocity (can be defined in the script), angle (also can be defined in script), user-defined argument);
For the user defined argument, any value or text string you put there can be transfered to the enemy script by saying GetArgument in the enemy script. So if I said 2 as the last parameter of my CreateEnemyFromFile, in the enemy script I could say:
v = GetArgument;
And v would assume a value of 2 for the script.
Objects are more flexable in terms of being able to share the same global variables with the main boss, as well as being able to get the coordinates of the boss, but Enemy Scripts are much easier to handle and are probably much easier to code advanced movements with. Choose whichever you fancy, but keep in mind that spawning familiars as enemies can have unexpected results (some player script homing shots may target it instead of the boss, etc).
How to make a boss fight!
A Plural file is a .txt file that allows you to string together multiple scripts to make a boss fight. Essentially it operates by playing your scripts in the order you define in the file. It also manages how the boss' life bar will be broken up into sections. Plural files look like this:
#TouhouDanmakufu[Plural] #Title[ExRumia Boss Stage] #Text[ExRumia boss fight, including regular attacks and spell-cards.] #Image[.\img\ExRumia(?¯•„?uƒ~ƒbƒhƒiƒCƒgƒŒƒ”ƒ@ƒŠƒG?v).png] #BackGround[Default] #Player[FREE] #ScriptVersion[2]
#ScriptPathData #ScriptPath[.\ExRumia01.txt] #ScriptPath[.\ExRumiaSpell01.txt] #ScriptPath[.\ExRumiaSpell02.txt] #ScriptNextStep #ScriptPath[.\ExRumia02.txt] #ScriptPath[.\ExRumiaSpell03.txt] #ScriptPath[.\ExRumiaSpell05.txt] #ScriptNextStep #ScriptPath[.\ExRumiaSpell04.txt]
#EndScriptPathData
Alrighty, the first little bit should be self-explanatory. Declare that this is a TouhouDanmakufu script, indicate that it is a plural script on the same line, set a title for the script, set some descriptive text, set an image to appear when you're selecting the script from Danmakufu's menu, set the background, set what player characters may be used, then indicate it's script version two.
Now, onto some of these new parameters.
#ScriptPathData starts the script path data (no wai). Be sure to declare this at the beginning of this string, it tells Danmakufu that the following is data for your plural script.
#ScriptPath[] tells Danmakufu to load and play the script you indicate in it's braces. It's relative to this file, so if your scripts are located in a subfolder, be sure to show that with [.\Sub Folder Name\script.txt]
#ScriptNextStep tells Danmakfu how to arange the life bars at the top of the screen. Everytime you call this, a new life bar is broken up and created, so all the scripts inbetween #ScriptNextStep will appear on the same lifebar. Test it out to see what I mean.
#EndScriptPathData does exactly what it says. Declare this at the end of your plural script.
And that's about it. Playing this script will tell Danmakufu to play all the scripts you've indicated inside this file, in decending order. So in the example, ExRumia01.txt will be played first, then when that script ends, ExRumiaSpell01.txt will be played, then ExRumiaSpell02.txt will be played, then a new lifebar will be created, then we'll move onto ExRumia02.txt, etc.
How to make Stages!
Start with your usual crap in any Danmakufu script, except this time you're specifying that this is a stage script:
#TouhouDanmakufu[Stage] #Title[Stage script tutorial] #Text[How to make stages in Danmakufu] #Image[] #Background[] #BGM[] #Player[FREE] #ScriptVersion[2]
You should know what everything here means, just make sure you've specified Stage after the first line.
Alrighty, onto how to script full-length stages. It's not very difficult actually, we just need to understand a few things beforehand. We actually don't even need to understand how tasks work, we just need to know how to use them. I won't bother explaining tasks and yield;, all of that is explained in Iryan's section of this tutorial. It's not necessary to know for this, but it certainly helps.
So the bulk of the code will go inside script_stage_main{}. This is how it plays out:
script_stage_main{
@Initialize{
} @MainLoop{ yield; } @Background{
} @Finalize{
} }
Nothing unfamiliar. How this stage will play out will not be written in @MainLoop, but instead inside of a task. This way we can dictate exactly when things will occur by waiting a certain amount of frames before doing something else -- here's how we do that: We'll make a task just after script_stage_main and just before @Initialize, and call the name of that task in @Initialize, like so:
script_stage_main{
task stage{
}
@Initialize{ stage; } @MainLoop{ yield; } @Background{
} @Finalize{
} }
This tells Danmakufu to run stage once at the very start of the script. So this way, we begin running our stage task as soon as the script starts. Let's declare some helper functions to make timing things in the stage task a little easier. We'll put these just before task stage{} so we can see them before anything else.
script_stage_main{
function Wait(let frames){ loop(frames){yield;} } function WaitForZeroEnemy{ while(GetEnemyNum != 0){yield;} } task stage{
}
@Initialize{ stage; } @MainLoop{ yield; } @Background{
} @Finalize{
} }
Alright, we've made two functions. The first is called Wait and requires us to declare a parameter everytime we call it. What we've made it do is pause the script for however many frames we tell it to. So saying Wait(60); will tell Danmakufu to stop reading anything beyond it for 60 frames (1 second). The second function is called WaitForZeroEnemy, and doesn't require us to declare any parameters. It tells Danmakufu when there are enemies on the screen (when the enemy count does not equal zero), wait. This is useful if you want the player to kill all the enemies you've spawned before continuing any further with your stage.
These functions will actually have use if we start putting things between them! So, let's get to spawning some enemies. The way we spawn an enemy is simple:
CreateEnemyFromFile(path of enemy script, x-coordinate, y-coordinate, velocity (if you didn't declare it in the script), angle (if you didn't declare it in the script), user-defined arguement);
These 6 parameters are simple: The first is the pathname of your enemy script, the second and third are the x and y coordinates of where you want your enemy to spawn, the fourth parameter is the velocity you want the enemy to have when you spawn it (you can set it to 0 and just define the velocity in the enemy script if you'd like), the fifth is the angle you want the enemy to move towards (again, can be defined in the script), and the last is a user-defined arguement. If you've forgotten my previous explanation, here it is again: any value you want to pass on to the enemy script you put into the last parameter. You get this information by saying GetArgument in the enemy script. So, if I said 2 for the last parameter in CreateEnemyFromFile, in the enemy script I could say:
v = GetArgument;
And then the variable v would assume a value of 2 for that enemy script. Useful if you want to change bullet angles or something but still use the same enemy.
Alright, I've done alot of explaining about enemy scripts, yet you don't even know how to create them. Well, good knews! They're exactly the same as what you've been creating all along, except with even less information! In an enemy script, you can neglect anything beginning with # (like #TouhouDanmakufu, #Title, etc), and just get straight to script_enemy_main. In that, it's just the same shit, but a different pile. You include your @Initialize, @MainLoop, @DrawLoop and @Finalize just like a boss script. Load graphics, spawn bullets, set life, all of it you do exactly the same. For enemies, you won't want them to have too much health, so you can kill them quickly. Typically have them spawn a really basic flurry of bullets, and just continue off in one direction (they will be destroyed upon leaving the game screen).
So all that information is nice, but how do we throw it all together into a stage? Let's take a look:
#TouhouDanmakufu[Stage] #Title[Stage script tutorial] #Text[How to make stages in Danmakufu] #Image[] #Background[] #BGM[] #Player[FREE] #ScriptVersion[2]
script_stage_main{
function Wait(let frames){ loop(frames){yield;} } function WaitForZeroEnemy{ while(GetEnemyNum != 0){yield;} } task stage{ Wait(120); CreateEnemyFromFile(GetCurrentScriptDirectory~"enemy.txt", GetCenterX, GetCenterY, 0, 0, 0); CreateEnemyFromFile(GetCurrentScriptDirectory~"enemy.txt", GetCenterX, GetCenterY, 0, 0, 0); WaitForZeroEnemy; CreateEnemyFromFile(GetCurrentScriptDirectory~"enemy2.txt", GetCenterX, GetCenterY, 0, 0, 0); WaitForZeroEnemy; CreateEnemyBossFromFile(GetCurrentScriptDirectory~"cirno.txt", 0, 0, 0, 0, 0); WaitForZeroEnemy; Wait(60); Clear; }
@Initialize{ stage; } @MainLoop{ yield; } @Background{
} @Finalize{
} }
Let's go through how this stage will play out. First, the stage will wait two seconds before anything happens. Then, it will spawn two enemies at the center of the screen whose behavior I've defined in enemy.txt, which is located in the same script directory as my stage file. The stage script will then wait until the player kills those two enemies before spawning a different enemy, enemy2.txt. The script will again wait for the player to kill the enemy, then it will create a boss. The CreateEnemyBossFromFile function is exactly the same as CreateEnemyFromFile function, except you can summon plural files as well, and the enemy will be treated as a boss (will have an enemy marker, can use spellcards, etc). The stage script will then wait for you to defeat the boss (can be a multi-spellcard plural file), then end the stage with the Clear function.
Make sure you include yield; in your @MainLoop for the stage file, this allows all the other yield; commands to work correctly.
And that's it. This stage is incredibly small, since most stages spawn hundreds of different kinds of enemies, which require seperate enemy files or really complex GetArgument statements. The coding is long and tiring, but it looks freakin' sweet when you finish one off. Good luck!
How to make Event scripts!
Alrighty, for event scripts, you're gonna have to go beyond script_enemy_main{} inside of a spellcard and write the following:
script_event name{
@Initialize{ [Load all your graphics and foolishness here] }
@MainLoop{ SetChar(LEFT, [player graphic]); //Set the player's character on the left side, with the graphic you've told it to display. SetGraphicRect(LEFT, [left-side], [top-side], [right-side], [bottom-side]); //The region you're displaying of the graphic for the player character, just like SetGraphicRect. MoveChar(LEFT, BACK) //Move the player's character into the background, to show she is not speaking. SetChar(RIGHT, [enemy's grahic]); //Set the boss' picture on the right side of the screen. SetGraphicRect(RIGHT, 0, 0, 200, 350); //Set the boundry of the picture you want displayed. MoveChar(RIGHT, FRONT); //Move the boss' image to the front to show that she is speaking. TextOutA("The text you want the character to speak goes here"); //Self explanatory. Danmakufu will not pass this function until a certain amount of time has passed, or the player clicks the shot button. MoveChar(RIGHT, BACK); //Move the boss to the background, then... MoveChar(LEFT, FRONT); //Move the player forward, to show that she will now speak. TextOutA("More words here"); //What the player will be speaking. End; //This ends the event. }
@Finalize{ [delete all your graphics here] } }
Keep in mind that anything after // in Danmakufu is a comment and will not be processed, so I can explain the functions as they appear in the code.
To initialize the event, call in your main script:
CreateEventFromScript("name");
Which will tell Danmakufu to look in the current file for an event script called name, then run it.
For your TextOutA commands, there are two special character combinations that will tell Danmakufu to do different things. The first is /n, which will tell Danmakufu to write everything proceeding it on a new line. The second is /c[COLOUR], which tells Danmakufu to write everything proceeding it in the designated colour. Colours available are the standard bullet colours: RED, GREEN, BLUE, YELLOW, PURPLE, AQUA, and WHITE.
A few other text options are available, including TextOutB, which has two parameters:
TextOutB(time text is displayed in frames, "text you want to be displayed");
As you could guess, this is the same thing as TextOutA except is displayed for a certain amount of frames. Think about the midboss text for the Extra stage of any Touhou game. The text can't be skipped and is displayed for a certain amount of time, which you designate in the first parameter. As per TextOutA, /n and /c[COLOUR] are available.
Select is another text function for event scripts, and is used for choosing two different options. Think stage five Imperishable Night (go versus either Eirin or Kaguya by choosing path A or path B). There are two parameters for Select:
Select("Option number 1", "Option number 2");
The function will then output a value based on which option the player chose, either 1 or 2. So to use this function in conjunction with different things happening, you could say:
if(Select("Stage 1", "Stage 2")==1){ [code for "Stage 1"] }else{ [code for "Stage 2"] }
And that's about all you need to know for event scripts. Happy talkin'!
Thanks for reading the Danmakufu Intermediate English Tutorial, hopefully you've learned a few things that will help you become a better coder with Danmakufu. Nuclear Cheese's Shot Object TutorialSample Files: 1,2- Spoiler:
Section 0: Before You Try This
This tutorial assumes you have a basic familiarity with danmakufu scripting in general. If you don't understand that yet, this tutorial won't really help you. Sorry. I recommend looking at other references that are available at this site and the main Touhou wiki.
Section 1: The Very Basics
First, download the sample files using the link above, and extract the files somewhere in your danmakufu scripts directory.
For this section, we will be using the first script file, objshot1.txt
Important Commands
Obj_Create ( <Object Type> ) - This function creates an object. For now, we're going to only use the type OBJ_SHOT. An important thing to note is that this function returns the ID of the object, which you need in order to make any changes to it later.
Obj_SetPosition ( <Object ID> , <X> , <Y> ) - Sets the position of an object.
Obj_SetSpeed ( <Object ID> , <Speed> ) - Sets the speed of an object.
Obj_SetAngle ( <Object ID> , <Angle> ) - Sets the angle of an object. Note that this is unaffected by SetShotAngleType ...
ObjShot_SetGraphic ( <Object ID> , <Graphic> ) - Our first object shot-specific function, this will set the graphic of the shot (for instance, RED21).
Creating a Shot
Using these commands to create a single object shot is demonstrated in objshot1.txt
The process is a bit more that just creating normal shots. The following must be done to create an object shot:
1) Create the object using Obj_Create. Remember to save the ID in a variable. 2) Set the object shot's position, speed, angle, and graphic, using the appropriate commands.
The script just creates a single object shot every few seconds, aimed straight downwards. It doesn't look like much, but with a bit of tweaking we can see the true power of object shots.
Section 2: The Usefullness of Object Shots
Object shots have a huge advantage over other types of shots - you can reassign their information whenever you want, so long as you keep the ID value.
For this section, refer to the script file objshot2.txt
This shows one of the many possible applications of object shots - it creates a single shot that relentlessly chases the player.
Important Commands
Obj_GetX ( <Object ID> ) - This will returns the indicated object's current X position.
Obj_GetY ( <Object ID> ) - This will returns the indicated object's current Y position.
Obj_BeDeleted ( <Object ID> ) - This will return true if the indicated object has been deleted by danmakufu for some reason.
An object can be deleted for several reasons, but there are three reasons that will occur regularly for object shots:
* The shot flies off of the screen. Danmakufu deletes shots that go far enough off of the screen automatically, to save processing time and memory. * The shot gets erased by a player's bomb * The shot gets erased because the player got hit and lost a life.
Checking if an object has been deleted is important, since you can't do anything with shots that no longer exist.
In this script, since only one bullet is used at a time, this command is used to check if that bullet has been deleted and, if it has, reset the script's countdown variable so that we spawn a new bullet.
The Bullet Just Wants a Hug!
There are a few pieces to this script's @MainLoop.
The first piece simply keeps decrementing the count variable while it isn't negative. This counter variable coordinates the script - we fire our shot when it hits zero, and then it goes negative so we know to manipulate the shot.
The second piece creates the shot when count reaches zero. This is exactly the same as in the previous script, except that this time we save the object shot's ID in a script-global variable - obj_id - so we can reference it later.
The third piece - which checks if (count < 0) - is where we manipulate our object shot.
The first part of this is that we check if our object shot has been deleted. If it has, then all we do is reset count to a positive value so we can trigger the script to fire another shot.
If the shot has not yet been deleted, however, we change its angle to point at the player. This is what the atan2 stuff does - the math here for the variable new_angle simply calculates the angle from the object shot to the player.
Notes
* Most of the functions we've used so far are applicable to all objects, not just object shots. This tutorial focuses on object shots, though, so we're only going to focus on how these functions are used with object shots for the time being.
Section 3: Arrays of Object Shots
Of course, all this work would be for naught if it didn't allow us to spam hundreds of these shots on the screen at once, right? But, in order to deal with many object shots, you need to store all of their ID values. This is where arrays come in.
Sit up and pay attention, students, 'cause this is where things get interesting!
A (Somewhat Optional) Crash Course in Arrays
Note: this section isn't intended to fully describe what you can do with arrays. It just gives what's needed for our work with object shots. Don't worry too much if its a bit confusing, though, since the code I will preset is simple, and can be mostly copy-pasted and tweaked as needed.
An array is a special type of value for a variable; unlike a simple number, an array holds a list of items. For instance, the code
let an_array = [1, 2, 4, 3];
creates an array with four elements, the numbers 1, 2, 4, and 3 in that order.
To access a value in an array, use the variable's name, followed by the number of the item you want to access inside of square brackets:
let a = an_array[1];
Assuming an_array was defined as above, this will set a to the value 2.
It is important to note that that the first item in an array is numbered zero, and the last item is always one lower than the total count of items in the array. In this case, the first item is numbered 0 and the last item is numbered 3.
You can use the length function to get the length of an array:
let b = length(an_array);
This sets b to 4 in this case, referring to the number of items in the array.
You can add an element on to the end of an array with the ~ operator:
an_array = an_array ~ [7];
This will redefine an_array to have a fifth element, with the value of 7, added to the end of its list.
To remove an item from an array, just use the erase command:
an_array = erase(an_array, 1);
This will remove the value 2 (the item numbered 1, a.k.a. the second value in the array) and shift all of the other values left to fill in the space.
Note: it can be a common mistake (I do it regularly :V ) to forget the an_array = part of this formulation; this will cause your array to not get the update. Make sure to write this properly when working with arrays.
Arrays of Object Shots
For this section, I will refer to another sample file. This is an old script I did to demonstrate this concept: Random Sign 「Entropy」
This script creates many object shots, which will randomly change shape and direction occasionally. There is a bunch of stuff here that is beyond the scope of this tutorial, but the core can be worked down to a few simple parts.
Initialization
First, you need to declare your array of object shot IDs. This is pretty simple:
let bullets = [];
Note that [] is an empty array (an array that has zero items in it) - in other words, we start out with no object shots. Makes sense, right?
None of the other variables in the sample code directly affect our object shot array; the code is pretty heavily commented if you want to understand more of it, though.
Adding a Shot
The next important piece of the puzzle is adding an object shot. In the sample code, this occurs right at the beginning of @MainLoop whenever the variable count hits zero. The code is pretty much exactly the same as what we've seen before, with Obj_Create and such. The only new command is ObjShot_SetDelay, which simply sets the delay before a shot becomes "active".
The important part for this tutorial is in this line:
bullets = bullets ~ [obj];
Here, bullets is our array of object shot IDs, defined earlier, and obj is the ID of our newly-created object shot. This adds the object ID to the end of the array; over time, the array will build up a large list of these IDs, which we will use in the next piece.
Manipulating the Shots
The last chunk here is also the biggest, but most of it is pretty straightforward. The framework of this is as such:
let i = 0; while (i < length(bullets)) { if (Obj_BeDeleted(bullets[i])) { bullets = erase(bullets, i); i--; } else { let obj = bullets[i]; // Do object processing for the object identified by obj } i++; }
The loop counts through all of the items in the object shot ID array, bullets. For each one, it does the following:
* If the object shot has been deleted by Danmakufu ... o Delete its ID from the list, since it is no longer useful to us o Push the variable i back one. Simple explaination for this is that if we don't do this we'd skip the next element. Don't worry about it for now, just do it. * Otherwise, if the shot has not yet been deleted ... o Get the shot ID stored in the array item numbered i, and store it in the local variable obj o Do any processing on the object shot. This is handled just like in Section 2.
In the sample script, this code starts about halfway down into @MainLoop and continues until just before the end.
Basically, just copy this structure into your @MainLoop and fill in the per-shot processing and it will do its work on all of the shots in the array.
IMPORTANT NOTE: If you forget the i++ at the end of the while loop, your script will freeze Danmakufu ... please be careful about this.
Summary
To work with an array of object shots, you just need to add a few pieces to your script:
# Initialize an array for object shot IDs # When creating an object shot, make sure to add its ID to the array # Use the loop code above inside your @MainLoop, and fill in your code to modify the object shots as desired
A Couple Ideas
The main limitation on this technique is how creative you get with it. A couple sample ideas that could easily be done using this setup:
* Spawn a bunch of bullets that curve left or right, based on a variable that changes every once in a while o Quite simply, the object processing would be to adjust each object shot's angle based on the current angle change rate. * Spawn a bunch of bullets, then have them all target the player at once after an amount of time o This is a bit trickier. First, the loop to process object shots will be inside of a condition, so it only triggers once per wave of the attack o The object processing would be to re-aim all of the shots toward the player's current location. For added effect, you can change the appearance as well.
For even more interesting ideas, you can try setting up multiple arrays of object shots in the same script. Remember that each set of object shots needs its own array of IDs (replace all references to bullet with another name for your second, third, etc. set ...).
All resources used in this Article can be found on Danmakufu Wiki
Last edited by ZeiZou on Mon Jun 28, 2010 1:22 am; edited 1 time in total | |
| | | Tsun[Art]Dere
Posts : 172 Join date : 2010-06-06
| Subject: Re: Toho Danmakufu Sun Jun 27, 2010 7:23 pm | |
| tl;dr, what does it do in layman terms? | |
| | | ZeiZou
Posts : 108 Join date : 2010-06-06
| Subject: Re: Toho Danmakufu Sun Jun 27, 2010 7:29 pm | |
| It helps you script games.
Also. its in sections. ._. | |
| | | Sakurasaki
Posts : 101 Join date : 2010-06-06 Age : 31 Location : Hakurei Jinja
| Subject: Re: Toho Danmakufu Wed Jun 30, 2010 11:09 am | |
| Stoopid art. Danmakufu is an absolute art! | |
| | | Sponsored content
| Subject: Re: Toho Danmakufu | |
| |
| | | | Toho Danmakufu | |
|
| Permissions in this forum: | You cannot reply to topics in this forum
| |
| |
| |
|