Site Logo


Sparen's Danmakufu ph3 Tutorials Lesson 23 - Basics of Stages in Danmakufu

Part 1: What will be Covered in this Lesson?

In this lesson, I will discuss Stage Scripts - in particular, how they function and how they can be used.

Part 2: What are Stage Scripts?

Stage Scripts are the main structural component of a full stage in Danmakufu. Just like Singles correspond to a single attack and Plurals correspond to a boss battle, Stages encompass various different components that make up a stage. In a Stage, you will load Plural scripts, handle creation of stage enemies, and handle stage banners, various graphical effects, and store commonly used events.

Over the course of this unit, we will cover Common Data and its applications, @Event and how it can be used to run specific code from anywhere in your game, stage enemies and familiars, and the basic components of 3D backgrounds, user input, and player scripts.

Stage scripts in Danmakufu are denoted by #TouhouDanmakufu[Stage] and contain the same basic components as other scripts we have seen. It is HIGHLY RECOMMENDED that you be familiar with all prior lessons before entering this unit. Stage scripts are extremely versatile and you can do practically anything in them. In fact, continue systems and the like heavily depend on stages.

Part 3: How do Stage Scripts work?

Like any other script, Stages can be run in the standard manner, and they also have special code in packages for various stage control functionality. Like other scripts, they will run in parallel with the System, Background, loaded and registered plurals, and more.

Stage scripts, in order to perform their tasks, will depend on task calls to run in parallel, control mechanisms to ensure that scripts are kept in sync, and careful use of functions and subroutines for appropriate blocking. In this brief lesson, we will observe the default ExRumia_Stage.txt that comes with ph3 [.1 pre6a].

#東方弾幕風[Stage]
#ScriptVersion[3]
#Title["EXルーミアテストステージ"]
#Text["EXルーミアテストステージ"]
#Image["./ExRumia(星符「ミッドナイトレヴァリエ」).jpg"]
#Background["script/default_system/Default_Background_IceMountain.txt"]

@Initialize
{
    TStage();
}

@MainLoop
{
    yield;
}

@Finalize
{
}


task TStage
{
    let dir = GetCurrentScriptDirectory();

    //ボス再生
    let path = dir ~ "ExRumia_Plural.txt";
    let idScript = LoadScriptInThread(path);
    loop(60){yield;}//1秒くらいあれば、コンパイル完了すると思われる。
    StartScript(idScript);

    //敵ボスシーンが終了するまで待機
    while(!IsCloseScript(idScript) && GetPlayerState() != STATE_END)
    {
        yield;
    }

    //~~~敵の出現やボスの出現を繰り返す。

    loop(240){yield;}

    //ステージ終了
    CloseStgScene();
}

Part 4: How do I run a Plural Script from a Stage Script?

We will begin by observing @Initialize, where a single task is called. This TStage task is the main task of the Stage script, and contains essentially everything we will need to run. The @MainLoop has a yield; as is standard to allow the TStage task to run.

In TStage, we define a path to a Plural Script, then run LoadScriptInThread(). This loads the script (blocking, waits for the script to load before doing anything else), and returns the ID of the now-running script. This ID is important for NotifyEvent, which we will cover in a later lesson.

StartScript() is used to run the script with the given ID. Note that it doesn't particularly matter whether or not the script was a Plural script - any script can be run this way, hence the versatility of Stage scripts. Note that you can technically load and run scripts elsewhere but Packages, Stages, and System scripts are the three places where external scripts are typically run.

After starting the other script, which runs in parallel, the stage scripts waits for two conditions - the player being alive and the plural script being open. Once either ceases to be true, the stage waits a bit and then closes - otherwise it stays open, waiting for the other conditions to be met. Both conditions are important - if there was no check for the plural being open or not, the stage would just close without waiting for the plural to complete. If the player death check is not present, if the player died in the plural the stage script would continue onwards.

At the end of every stage, it will be necessary to CloseStgScene, which closes the stage formally. And with that, we have a simple stage.

Now let's spice things up a bit, shall we?

Part 5: How do I play music in a Stage Script?

If you are not familiar with Danmakufu's sound functions, please refer to Lesson 15 and refresh yourself.

To begin, have a stage script prepared as well as two .ogg music bgm tracks. We will create a new function in our Stage Script as follows, similar to the CreateTrack task in Lesson 15:

function StageBGM(obj, ID){
    ObjSound_SetSoundDivision(obj, SOUND_BGM);
    ObjSound_SetRestartEnable(obj, true);
    ObjSound_SetLoopEnable(obj, true);
    ObjSound_SetLoopTime(obj, 0, 300);
    if(ID == 1){ObjSound_SetLoopTime(obj, 0, 32);}
    if(ID == 2){ObjSound_SetLoopTime(obj, 32, 192);}
    return obj;
}

We will use this code to handle two different tracks in the stage - the stage theme and the boss theme. Note that all this function does is set values for a provided object - it DOES NOT create the sound object itself.

At the start of TStage, we will want to actually create and process the two sound objects, as well as the boss script. NOTE: bgm and bgm2 have been declared outside all the routines in this scenario.

task TStage{
    let pathboss = GetCurrentScriptDirectory() ~ "./script/PS1B.txt";
    let bossScript = LoadScriptInThread(pathboss);

    bgm2 = ObjSound_Create();
    ObjSound_Load(bgm2, GetCurrentScriptDirectory() ~ "music/Boss1.ogg");
    StageBGM(bgm2, 2);
    bgm = ObjSound_Create();
    ObjSound_Load(bgm, GetCurrentScriptDirectory() ~ "music/Stg1.ogg");
    StageBGM(bgm, 1);
    ObjSound_Play(bgm);
    //...

Note that I am loading the boss script at the start of the stage. This brings us errors with the plural at the start of the stage and has other benefits. Below this, we create and prepare two music tracks, and then play the stage bgm.

It will generally be advisable to load all your assets at the start of the stage rather than right before they are needed.

Although we have a partially functional system now, the above is only one piece of the bigger puzzle, as we need to provision for when the player pauses the game - when the player pauses the game, you will generally want the background music to stop, and you will want the music to resume once the pause has ended.

In this stage, I am using the stagepart variable to store information on which part of the stage (and therefore music track) is currently active - 0 for stage and 1 for boss. This value will be initially set to 0 and then set to 1 once the boss music begins playing.

There are two events relating to pausing - EV_PAUSE_ENTER when the user hits the virtual key for pause (defaults to esc) and EV_PAUSE_LEAVE when a pause script is closed. Using the system below, we are able to turn off the correct bgm track and then turn it back on after the pause script has run.

@Event {
    alternative(GetEventType())
    case(EV_PAUSE_ENTER){
        if(stagepart == 0){
            ObjSound_Stop(bgm);
        } 
        if(stagepart == 1){
            ObjSound_Stop(bgm2);
        } 
    }
    case(EV_PAUSE_LEAVE){
        if(stagepart == 0){
            ObjSound_Play(bgm);
        } 
        if(stagepart == 1){
            ObjSound_Play(bgm2);
        } 
    }
}

The final step is to actually handle the transition to the boss. If you have a dialogue event, you will want to handle this transition within the dialogue event, but otherwise transitioning immediately before the boss is quite effective. Given the system I have provided above, all we need to do is change the stagepart to 1, and then switch to the other bgm track.

        stagepart = 1;
        ObjSound_Stop(bgm);
	ObjSound_Play(bgm2);
        //Handle starting boss plural script

And that's all!

Part 6: How can I have both a boss and a midboss in a Stage Script?

As an extra note, the process for handling multiple plural scripts in a single stage script is quite simple. Assuming the midboss doesn't have any special music needs, we can do the following:

task TStage{
    let pathmidboss = GetCurrentScriptDirectory() ~ "./script/PS4M.txt";
    let pathboss = GetCurrentScriptDirectory() ~ "./script/PS4B.txt";
    let midbossScript = LoadScriptInThread(pathmidboss);
    let bossScript = LoadScriptInThread(pathboss);

    //Handle stage

    //Midboss
    StartScript(midbossScript);
    while(!IsCloseScript(midbossScript) && GetPlayerState() != STATE_END){
      yield;
    }

    //Handle stage

    //Boss
    StartScript(bossScript);
    while(!IsCloseScript(bossScript) && GetPlayerState() != STATE_END){
      yield;
    }

    //Cleanup

As can be seen from the code above, it's a simple matter of loading another plural and then starting it when necessary.

Quiz: Stage Scripts

1) Keine has a script that does not seem to work correctly. The plural starts, and then the player beats the boss. However, the stage never progresses? What could the problem be?

A. The Stage script does not check whether or not the plural ended.
B. The Plural Script does not have a CloseScript(GetOwnScriptID());
C. The player Mokou-tan actually died.

2) True or False: Stages can create Primitive and Move objects.

A. True
B. False

Summary

  • Stage Scripts can load Plural Scripts with LoadScriptInThread() and run them with StartScript()
  • Stage Scripts are closed using CloseStgScene
  • Stage Scripts can be used to handle graphical effects and music for stages and bosses

Sources and External Resources

N/A