Site Logo


Sparen's Danmakufu ph3 Tutorials Lesson 7 - Attack Flow and Our First Spellcard

The video for this lesson is Lunarethic's AllSpell Youmu! Since we're going to be talking about spells (though that's not the focus of the lesson), what better option than this? (And yes, I know that I'm showing some of the best scripts I have played for DNHArt. There will come a time when other scripters will be featured)

Part 1: How Do I Designate a Single as a Spellcard?

To begin, a note. I will not be involving the @MainLoop style from now on - I will show the task method and my own method, because the former is commonly used and because the latter is good for understanding alternate ways in which you can script Danmakufu. Also, it is highly recommended that you have read Lessons 3-6 and understand most of the information.

Let's start off with our code from the end of Lesson 6. We will now begin the transformation from a nonspell to a spell!

First, let's review the top of our code - the part containing global variables. Currently, we only have let objBoss;. Right after that, we will want to add the following:

    let objScene = GetEnemyBossSceneObjectID();

This will allow us to access the Enemy Boss Scene Object. We will not usually do anything with this object, but it should find a comfortable place in your global variable section and we will directly use it in all spellcards.

Now we will look at @Initialize. Please note that there is no default cutin function in ph3. I suggest using GTbot's or Helepolis's, which can be found on MotK or Bulletforge. Additionally, you can make your own, but we will need understanding of text objects and render objects before we can go into that.

The most important line we will add to @Initialize is ObjEnemyBossScene_StartSpell(objScene). Add it before you call TFinalize. This code will tell Danmakufu that we are now starting a spellcard. Additionally, we will begin cleaning up @Initialize. I suggest taking all the boss drawing code (the imgRumia stuff) and placing it in a task TDrawLoop and calling that in @Initialize. More specifically, we will create a task TDrawLoop defined outside of all routines and we will use TDrawLoop; in @Initialize to call it. This will allow us to animate the boss later on and will make @Initialize look much cleaner. Please do not move the movement code into TDrawLoop. While it is not detrimental, you may end up not being able to find the movement code if you end up looking for it. Below is an example of what @Initialize should now look like:

@Initialize{
    objBoss = ObjEnemy_Create(OBJ_ENEMY_BOSS);
    ObjEnemy_Regist(objBoss);
    ObjMove_SetDestAtFrame(objBoss, GetCenterX(), 60, 60); //Move the boss to the specified position

    ObjEnemyBossScene_StartSpell(objScene);

    TDrawLoop; //Call TDrawLoop, which is defined outside of this block.
    TFinalize;
    MainTask;
}

task TDrawLoop {
    let imgExRumia = GetCurrentScriptDirectory() ~ "ExRumia.png";
    ObjPrim_SetTexture(objBoss, imgExRumia);
    ObjSprite2D_SetSourceRect(objBoss, 64, 1, 127, 64);
    ObjSprite2D_SetDestCenter(objBoss);
}

Now we will move to @Event. Thankfully, we already have EV_REQUEST_SPELL_SCORE, which is the spellcard bonus! So we don't have to do anything here. As a refresher, EV_REQUEST_LIFE sets the boss's life, EV_REQUEST_TIMER sets the timeout counter (in seconds), and EV_REQUEST_SPELL_SCORE sets the spellcard bonus if there is one. EV_REQUEST_IS_DURABLE_SPELL is optional and will designate the spellcard as a survival card.

Finally, to complete the transformation into a spellcard, there is an optional but highly recommended procedure that will go in TFinalize. What is it, you ask? Well, adding the spellcard bonus!

Danmakufu does not add the Spellcard Bonus automatically - you must add the spellcard bonus to the score manually. Thankfully, it's not hard to implement. Right after the while loop in TFinalize, add the following:

if(ObjEnemyBossScene_GetInfo(objScene, INFO_PLAYER_SHOOTDOWN_COUNT)
        +ObjEnemyBossScene_GetInfo(objScene, INFO_PLAYER_SPELL_COUNT) == 0){
    AddScore(ObjEnemyBossScene_GetInfo(objScene, INFO_SPELL_SCORE));
}

Basically, this code will check if the spell was captured (no bombs, no deaths). If it was, it will add the spellcard bonus.

As a final note, it is also possible to ObjEnemyBossScene_StartSpell(GetEnemyBossSceneObjectID) instead of having a global variable to store the EnemyBossSceneObject's ID, but you will have to use the GetEnemyBossSceneObjectID() function multiple times in the spellcard bonus code.

And with that, we have our basic spell done! Now to add stuff to it!

CHECKPOINT: What function tells Danmakufu that your Single is a Spellcard?

Part 2: What are the Movement Functions?

In our old script, all the boss did was sit there and shoot a bullet at the player every half second (30 frames). Now we will make the boss move!

For the tasking style, we will create a task called movement and call it right before the while loop in MainTask. We will also add a wait(120); before calling movement in MainTask. This will delay the task 120 frames before beginning to fire or move. The boss will be able to move into position, etc. For my style, set counter to -120 to start, and in the if statement controlling firing, add && count >= 0 so that bullets will only fire if count >= 0. As for movement, we can use the same movement task (I personally do this differently) as with the tasking style, which we will make below.

So, movement. For objects on which ObjMove functions apply (this will be discussed in Lesson 8), there are all kinds of ways to move the enemy. Documentation can be found here.

Firstly, we will not be using ObjMove_SetX(), ObjMove_SetY(), ObjMove_SetPosition(), ObjMove_SetSpeed(), or ObjMove_SetAngle(). Generally speaking, teleporting the boss halfway across the screen does not work well in playing, and using a speed/angle approach can be a royal pain to control. We will use these functions for positioning of things like bullets, but not for the boss. Instead similar to the initial movement of the boss, we will use the following: ObjMove_SetDestAtSpeed(), ObjMove_SetDestAtFrame(), and/or ObjMove_SetDestAtWeight(). For the parameters, you specify the object and the destination, and then specify either the speed to go at (constant for the entire journey), how many frames to take, or the weight and max speed, depending on which function you are using. Personally, I never use ObjMove_SetDestAtSpeed() because it is hard to synchronize anything else with it. ObjMove_SetDestAtFrame() is the best for syncing firing with movement. ObjMove_SetDestAtWeight() looks the most natural, although it requires more experience to control. We will use ObjMove_SetDestAtFrame() in this lesson.

In the task movement, we will now add a while loop just like the one in MainTask (it is also possible to put movement; inside the while loop of MainTask; and not include the while loop in movement; if using the tasking style). And then, we will add ObjMove_SetDestAtFrame(objBoss, rand(GetCenterX() + 90, GetCenterX() - 90), rand(GetCenterY() - 60, GetCenterY() - 120), 60); inside, as well as a wait(240);. This will move the boss every 240 frames to a random point within the bounds set. Note that the movement works parallel to what called it - that is, the boss will start moving when the function is called, and will finish moving 60 frames later, leaving 180 frames before the boss begins moving again.

Our movement task now looks like this:

    task movement{
        while(ObjEnemy_GetInfo(objBoss, INFO_LIFE) > 0){
            ObjMove_SetDestAtFrame(objBoss, rand(GetCenterX() + 90, GetCenterX() - 90), rand(GetCenterY() - 60, GetCenterY() - 120), 60);
            wait(240);
        }
    }

If using my style, then please make sure to include if(count == 0){movement;} in @MainLoop to actually call the task.

Now that our boss is moving, it is time for a short quiz, after which we will learn to do things more complex that firing a single bullet at the player.

Quiz: Frames, Speed, and yield;


1) How long does it take for a bullet traveling at speed 4 (pixels/second) to travel from the left side of the default playing field to the right side (same y coordinate)?

A. 96 frames
B. 160 frames
C. 112 frames

2) Minoriko wants her boss to move to the player's position every 180 frames. Which functions should she use?

A. ObjMove_SetPosition()
B. ObjMove_SetDestAtSpeed()
C. ObjMove_SetDestAtFrame()
D. ObjMove_SetDestAtWeight()

3) Shizuha wants to move the boss to a position every 45 frames, and she is syncing the spawning of bullets to this. Which function should she NOT use?

A. ObjMove_SetPosition()
B. ObjMove_SetDestAtSpeed()
C. ObjMove_SetDestAtFrame()

Part 3: How do I Use Loops?

Now we will begin making our code significantly more interesting.

For our current code, please check here.

Please note that for this part, we will focus on the MainTask; task for the tasking style and the fire; task for my style - we will not be touching any other parts of the script, although we will reference other parts.

For a refresher on loops, please refer to Unit 1 Lesson 5.

OK. So our current firing code is as follows:

    let angleT = GetAngleToPlayer(objBoss);
    CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 2, angleT, DS_BALL_S_RED, 5);

It's simple enough. We declare the variable angleT and set it equal to the output of the function GetAngleToPlayer(obj), which is the angle from the boss object to the player in this case. Then we fire a single bullet using CreateShotA1(). To start off, we will begin with rings, which are very simple.

To understand our approach, we have to consider some things. Firstly, what defines a ring? How many bullets do we want in the ring? Knowing that there are 360 degrees in a complete circle, how can we apply this to Danmakufu?

It may seem obvious that if there are n bullets in the ring and we want to make a complete circle, we should have 360/n degrees in between bullets. However, more complicated patterns are not so intuitive, and you will have to carefully figure out an approach to creating the pattern you wish to make.

Let's say we want 13 bullets in the ring. Therefore, we would increment our angle by 360/13. We will want to do this 13 times, spawning a bullet each time. So, how do we do this? Loops, of course! As a side note, you can technically use while loops and ascent/descent loops for this, but using ordinary loops is simpler.

    let angleT = GetAngleToPlayer(objBoss);
    loop(13){
        CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 2, angleT, DS_BALL_S_RED, 5);
        angleT += 360/13;
    }

The above code does exactly what we want. It will execute the code inside the loop block 13 times and will change angleT accordingly. As a result, we will end up with a ring of 13 bullets spawning from the boss!

However, do you find this to be somewhat... boring? Just one ring every 30 frames? Let's use ascent loops to make this code more interesting.

Remember that an ascent loop creates a temporary variable and that variable's value increments each time the loop executes. Using this, we can manipulate the speed and/or the delay of the bullet with ease.

Our current code is below. Let's say we want to have three rings, where the inner ones are slower than the outer ones, and where there are different bullet graphics.

    let angleT = GetAngleToPlayer(objBoss);
    loop(13){
        CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 2, angleT, DS_BALL_S_RED, 5);
        angleT += 360/13;
    }

For speed, an ascent loop is an excellent way to create stacked rings. For graphics, remember that we have imported a constant sheet that declares a number of final constants. However, these are simply more descriptive ways of referring to a bullet's ID - the IDs you call bullet graphics by are actually just numbers defined in a shotsheet.

If you look in default_system/Default_ShotConst.txt, it is clear that DS_BALL_S_RED is equal to 9 - this means that if we increment the graphic by one, we will get DS_BALL_S_ORANGE, which is 10, and then YELLOW, GREEN, SKY, etc. We will take advantage of this in our ascent loop.

    let angleT = GetAngleToPlayer(objBoss);
    loop(13){
        ascent(i in 0..3){
            CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 2.5 - i/3, angleT, DS_BALL_S_RED + i, 5);
        }
        angleT += 360/13;
    }

Please note that since I am decrementing the speed, I added 0.5 to the highest speed. Above is the code with the ascent loop. i will start at 0, and will have values of 1 and 2 inside the code block. For the speed, I have taken a base speed 2.5, and I decrement it by i/3. Since i has values of 0, 1/3, and 2/3, there will be three rings with different speeds. Additionally, I add i to DS_BALL_S_RED in the graphic section, resulting in each ring having a different graphic.

Of course, you can tweak this code however you want. Want less space between rings? Lower the difference in the speed. You can use i/6 for example to halve the distance between rings. Just beware of negative speed values - the bullet will go in the opposite direction.

For reference, my video tutorial on ascent loops can be found here

EXERCISE: Try spawning bullets in a semicircle - there's a trick to getting it just right! Hint: Use ascent loops
EXERCISE: Experiment with bullet speeds and delay in ascent loops - there's more you can do than just lines or waves of bullets.

Part 4: How do I Sync Spawning to the Boss's Movement?

Now we will add another attack to the boss's pattern. However, this pattern will look weird if spawned when the boss is moving.

For the tasking method, we will first create a task fireA, and will move the entire while loop in MainTask to fireA. In MainTask, where the while loop was located, call fireA; instead. We are naming it fireA because we will be creating a fireB later on. For the Sparen method, rename fire; to fireA. Remember to change it in @MainLoop as well. If your text editor supports find-replace all, you will quickly find the function to be a godsend.

Now, we will create a task fireB. You may want to copy fireA for now and simply rename it.

The pattern we are about to make (lines of bullets in a fan shape - I call them waves), tends to look ugli when fired while the boss is moving. Therefore, we will sync it to the boss's movement. Recall that the boss moves every 240 frames and is moving for the first 60 of those 240.

Therefore, we can spawn it the frame it begins moving and 120 frames after, when it is stationary.

For the tasking method, we will call fireB in MainTask; and replace the wait(30); with wait(120);. For the Sparen method, add the following to @MainLoop: if(count % 120 == 0 && count >= 0){fireB;}.

At this point, it is important to realize that this pattern will indefinitely spawn at the same time as the other pattern. Therefore, we will want to give it graphics that are different as well as a different speed. Additionally, if you wanted to spawn this pattern when the boss stops moving and 120 frames after, you can, for the tasking method, add a wait(60); before the while loop and, in the Sparen method, use count % 120 == 60 instead of count % 120 == 0.

Now for the pattern. We will be using a nested ascent loop for this, since we will have a fan of lines of bullets. Let's examine the following:

    let angleT = GetAngleToPlayer(objBoss);
    ascent(i in -1..2){
        ascent(j in 0..3){
            CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 3 - j/6, angleT + i*15, DS_RICE_M_SKY, 5);
        }
    }

This is the code that we will, for the tasking style, place inside the while loop instead of the ring spawning code and, for the Sparen style, place after the boss health check in fireB. As you can see, we are using an ascent loop inside an ascent loop (this is called 'nesting' loops). The first ascent loop uses the variable i, which increments from -1 to 2 and controls the angle. By using angleT+i*15 with i being -1, 0, and 1, we effectively create bullets aimed at the player as well as bullets 15 degrees to either side that are not aimed at the player. Next we will look at the inner ascent loop, which uses the same technique as before to control the speed of the bullets. It serves the same function, and effectively grants three separate waves of bullets. It's a pretty common technique that can be adapted to various situations.

This concludes the lesson. As of now, you now have the ability to do quite a few things in Danmakufu. I suggest practicing with the techniques you already have at this point so that you can better understand the techniques by experimenting with them yourself. However, I advise that you do not upload your scripts to Bulletforge at this point in the tutorial. Criticism can be harsh, and it's best that you wait until you have significantly more knowledge about Danmakufu. For information about Bulletforge, please read How to Use Bulletforge.

For the final versions of the scripts we have made in this tutorial, please check here.

CHECKPOINT: How long does it take for the boss to move to its destination when using ObjMove_SetDest functions? How can you synchronize this to multiple individual patterns?

Summary

  • Danmakufu will only recognize a spellcard if ObjEnemyBossScene_StartSpell(GetEnemyBossSceneObjectID()); is run
  • Danmakufu does not automatically add the Spellcard Bonus to the score at the end of a spell
  • Boss movement, animation, and attacks can be controlled via separate tasks that run at the same rate
  • You can use loops to create a variety of danmaku patterns

Sources and External Resources

[ 弾幕風 PH3 Tutorial ] Introduction to Danmakufu (Helepolis)
-->Not as relevant anymore, but still good if you haven't yet watched it

[ 弾幕風 PH3 Tutorial ] Improving our boss and danmaku (Helepolis)
-->Covers things that were also covered in this tutorial - a good resource.