Site Logo


Sparen's Danmakufu ph3 Tutorials Lesson 24 - Introduction to Common Data

Part 1: What will be Covered in this Lesson?

In this lesson, I will discuss Common Data on an introductory level. Area Common Data will be covered in a later guide. Before reading this tutorial, you should be familiar with variables and scoping, including terms such as global and local variables, as well as statements and blocks.

You should also be familiar with the standard scripts that run when you run a Danmakufu script, such as the background script and system script. If you are not familiar with these topics, please refer to earlier lessons to refresh yourself. There is a brief refresher below, but it is important to be familiar with the concepts in order to fully understand the use cases for Common Data.

Besides the basics of Common Data, I will also cover its most common usages in stage-tier scripts, those being configuration settings such as difficulty and communication between scripts - especially the system script and background script. There will be brief discussion on extend systems in a later guide.

Part 2: What is Common Data?

To understand Common Data, it is first necessary to understand variables and scoping, as mentioned prior.

For example, let's refer to the following:

let count = -60;

@Initialize {
    count = 0;
    DisplayCount(count);
}

@MainLoop{
    count += 1;
    yield;
}

task DisplayCount(inputcount) {
    let taskcount = 100;
    loop {
        WriteLog("Current Count: " ~ IntToString(count) ~ "; Input Count: " ~ IntToString(inputcount) ~ "; Task Count: " ~ IntToString(taskcount));
        taskcount += 1;
        yield;
    }
}

We will discuss the log window in a later lesson, but for now, all you need to know is that WriteLog prints the string it is given to the log window, where it can be seen. Recall that ~ concatenates two strings.

As can be seen from the code above, we have a global variable count, which is initially set to the value -60. In @Initialize, which runs once at the start of the script, the value of this global value count is reset to 0, and DisplayCount, a task, is called with the current value of count as a parameter. In our Main Loop, the global count value is incremented each frame.

Now, what does DisplayCount() actually write? Well, the first value printed is the value of the global variable count, which is 0, 1, etc. After the semicolon, it then prints inputcount, which is always set to 0. This is because Danmakufu passes by value rather than by reference (IE the value passed to DisplayCount when it was called is independent of the current value of the count global variable).

After those, it then prints the value of taskcount, a local variable to the DisplayCount task. As this is a local variable, attempting to call this value outside of the the block defining the contents of the DisplayCount task will result in failure.

But what if you need that taskcount value for something outside of DisplayCount? Well, within a script you would just make it a global variable, or increase its scope so that it can be accessed by more levels of nested blocks. But what if you want the value to be accessible by a different script?

One may think that #include files are a good solution. After all, if two different scripts running at the same time both include the same file, then of course any variables defined in that #included file should be shared, right? That is incorrect, as the way #includes work in Danmakufu involves their contents being read as if they were a part of the parent script that #included it. The two or more files that #include the same file will therefore have separate copies of the variables defined within the #include. So what's the solution?

Danmakufu provides CommonData as a means for communicating between multiple scripts. CommonData are shared across all running scripts, and can be read or written by any running script to a scope global to the entire project currently running. CommonData will persist until the game session has ended and Danmakufu is closed or the player has returned to the Danmakufu script navigation menus.

CommonData is therefore very useful, and utilizing it appropriately allows for great benefit and flexibility. However, using it poorly will result in even more confusing and hard to debug code, as issues from one script may persist in CommonData and affect subsequent scripts run in the same session. (Ex: CommonData set in one single accidentally changes behavior in the next single)

Part 3: How do I use Common Data?

To begin our discussion on the usage of Common Data, it is necessary for you to become acquainted with the functions used to set and retrieve Common Data. These can be accessed here.

For the purposes of this guide, we will only look at the four basic functions, which write to the 'blank' Common Data Area (more on this in the guide on Area Common Data). These are SetCommonData(), GetCommonData(), ClearCommonData(), and DeleteCommonData().

To begin, we will want to set our common data. The parameters for SetCommonData() are a key and a value; each CommonData area is essentially a map that maps keys (which must be strings) to values of arbitrary type.

Let's say that we are tracking the number of times the player has captured a spell in a stage to determine whether or not a special plural script should be run. One way to handle this would be to have a variable in the stage script and an Event that would increment the counter, but we will discuss this technique in a later guide.

With CommonData, we can have a CommonData with key 'NumSpellCaptures'. In our TFinalize for the plural (or whatever code is run at the end of the Single script), we can check for spell capture with the boolean expression ObjEnemyBossScene_GetInfo(objScene, INFO_PLAYER_SHOOTDOWN_COUNT) + ObjEnemyBossScene_GetInfo(objScene, INFO_PLAYER_SPELL_COUNT) == 0, assuming the current boss scene is referenced with the objScene variable.

We can wrap this boolean expression in an if statement, and we can increment the value stored in the CommonData accordingly.

    if(ObjEnemyBossScene_GetInfo(objScene, INFO_PLAYER_SHOOTDOWN_COUNT) + ObjEnemyBossScene_GetInfo(objScene, INFO_PLAYER_SPELL_COUNT) == 0){
        SetCommonData("NumSpellCaptures", GetCommonData("NumSpellCaptures", 0) + 1);
    }

Now, there are a few things to note. Firstly, it is not possible to simply increment with ++, as that functionality does not work on CommonData values, which can be of arbitrary type. Secondly, when using GetCommonData(), it is necessary to provide a default value in case that particular CommonData does not exist or has not been set yet. It is useful to default CommonData to helpful values, such as 0 for counters, -1 for values that are of critical importance and must be set, etc.

Typically, -1 is preferred as a default value for integers since most used integer values will be natural numbers (0, 1, 2, ...). If a natural number is expected and -1 is returned instead, then something must have gone wrong.

Beyond this, there are other functions - ClearCommonData(), and DeleteCommonData(). The former wipes all CommonData in the default 'blank' Common Data Area. For all practical purposes, if you are not using specific Area Common Data this will purge all key-value pairs that have already been defined, making it seem as if they had never existed. Given that this is automatically done when the script ends, there is rarely a time for this function to be used. DeleteCommonData will delete a single key-value pair with the specialized key. For example, if we were to DeleteCommonData("NumSpellCaptures") somewhere in our code, the next run of the code above would return the default value of 0 for GetCommonData("NumSpellCaptures", 0) regardless of what it used to be before running DeleteCommonData(). This is due to the fact that the key-value pair has been wiped and the mapping no longer exists after we run the deletion function.

Part 4: How can I use Common Data with Difficulties?

The most common use case for Common Data in stage level scripts is with difficulty selection. It is often more pleasant to have a single stage or plural to run that provides a difficulty select option rather than having a different executable plural for each difficulty.

We will cover the menu itself, communication with the System script to display a text object with the appropriate difficulty text and color, and usage of CommonData in the stage to select the proper plural (assuming there is a different plural for each difficulty).

Let's begin with the TStage from the sample stage:

//DNH Header and other routines have been excluded from this example.

@Initialize
{
    TStage();
}

task TStage
{
    let dir = GetCurrentScriptDirectory();

    let path = dir ~ "ExRumia_Plural.txt";
    let idScript = LoadScriptInThread(path);
    loop(60){yield;}
    StartScript(idScript);

    while(!IsCloseScript(idScript) && GetPlayerState() != STATE_END)
    {
        yield;
    }

    loop(240){yield;}

    CloseStgScene();
}

First, let's consider our basic menu. We want a menu at the start of the stage (with the stage background, likely) that will utilize the arrow keys to control the selection of options. During this time, the player cannot bomb, and it may be preferable to also disable player movement, though implementation of this restriction will, in true mathematics + computer science fashion, be left as an exercise to the reader :)

Let's begin. If you are unfamiliar with menus and basic virtual keys in Danmakufu, please review Lesson 20. Before doing any boss stuff, we will freeze the player's ability to bomb using SetForbidPlayerSpell(true) - this should prevent unwanted bomb usage.

Now, we will call our menu. Generally speaking, due to the fact that the menu must be blocking (IE, the stage cannot progress unless the player has completed selection of their options in the menu), we cannot use a task for the menu. Instead, we will use a function (a subroutine is also fine).

First, in our menu, we will want to create our text objects, give them IDs, and have a loop checking the player's current selection. See below for an example.

function SetStageDifficulty{ //Adapted from code used by gtbot
    let selection = 0; //Initialize the player's selection to index 0
    let sdiff = 0; //Selected difficulty
	
    let TextObjects = []; //Container for text objects, so that we can delete them at the end of the function
    let TextNames = ["Normal", "Hard"]; //Supported difficulties (text only)
	
    let diffs = [1, 2]; //Define the values (CommonData) to utilize 

    let Center = [GetStgFrameWidth()/2, GetStgFrameHeight()/2]; //Define the center of the playing field
    ascent(i in 0..length(TextNames)){ //For each difficulty option, handle the text object
        TextObjects = TextObjects ~ [DrawSubMenuText(TextNames[i])]; //Add the Text object to the array
        let locText = TextObjects[i]; //Retrieve text contents
        ObjText_SetHorizontalAlignment(locText, ALIGNMENT_CENTER);
        ObjRender_SetPosition(locText, Center[0] - 100, Center[1] - 150 + i*25, 0);
        ObjText_SetMaxWidth(locText, 200);
        ObjText_SetFontSize(locText, 16);//new
        ObjText_SetFontType(locText, "Helvetica");
        ObjText_SetFontBorderColor(locText, 16, 16, 16);
    }
	
    //Infinite Loop that runs until the player has made a selection.
    loop{
        //Menu controls
        if(GetVirtualKeyState(VK_UP) == KEY_PUSH){selection--;}
        if(GetVirtualKeyState(VK_DOWN) == KEY_PUSH){selection++;}
        if(selection < 0){selection = 1;}
        if(selection > 1){selection = 0;}

        //Make it clear to the player which option is selected
        ascent(i in 0..length(TextObjects)){
            if(selection == i){
                ObjText_SetFontColorTop(TextObjects[i], r[i], g[i], b[i]);
                ObjText_SetFontColorBottom(TextObjects[i], r[i]/1.5, g[i]/1.5, b[i]/1.5);
            } else {
                ObjText_SetFontColorTop(TextObjects[i], r[i]/1.75, g[i]/1.75, b[i]/1.75);
                ObjText_SetFontColorBottom(TextObjects[i], r[i]/2.25, g[i]/2.25, b[i]/2.25);
            }
        }

        //Handle setting Common Data here
        //SEE BELOW
	
        yield;
    }
    ascent(i in 0..length(TextObjects)){
        Obj_Delete(TextObjects[i]);
    }
}

In the above, we have a number of features, including text automatically centered horizontally, menu controls adjusting the selection, and color switching to make it clear which option is selected (it is highly recommended that you use an additional Render Object in addition to the color). In the case that the commands used for the text objects or the method for horizontally centering a text object are unclear, please review Lesson 17. Note that DrawSubMenuText() is a function that creates a text object, sets various fields, and returns the created object; it is not shown for conciseness.

Now we need to handle the actual setting of CommonData. Before the yield; in the infinite loop, we will do the following:

        if(GetVirtualKeyState(VK_SHOT) == KEY_PUSH){
            sdiff = diffs[selection];
            SetCommonData("Difficulty", sdiff); //Set the CommonData here
            break;
        }

Firstly, please note that in the method being showcased here, the "Difficulty" Common Data is a mapping from string to integer, with 0 corresponding to Easy, 1 to Normal, etc. Advantages of this mapping strategy will be discussed at the end of the current part.

In our original code above, we let diffs = [1, 2];. This is designed so that 0 can correspond to Easy and 3 can correspond to Lunatic, as stated above. The method shown here using sdiff enables any number of difficulties to be used without changing the { 0: "Easy", 1: "Normal", 2: "Hard", 3: "Lunatic" } mapping currently in place. As the scripter, it will be up to you to handle your difficulty mappings.

In regards to variables, selection is current selection of the text objects, indexed with the top text object (in this case, 'Normal') at 0. When the player chooses their desired difficulty, their selection uses the selection index to index the diffs array so that when the first 'Normal' option is selected, the selection index (0) indexes the diffs array and retrieves 1, which we have defined to be 'Normal' difficulty. If 'Hard' were selected, the selection index would be 1, would index diffs to retrieve 2, and would correspond to the 'Hard' difficulty.

With this, we now have a functional menu that allows the player to select their desired difficulty and save the result in CommonData.

After calling all menus, don't forget to re-enable player bombs.

Now, as for using this CommonData, a common technique, especially among new players, is to have different plural scripts for each difficulty. With CommonData and a menu, it is possible to have a single stage executable that handles all the difficulties in your script. To handle different plurals depending on the difficulty, in TStage, simply check the CommonData and run a different plural depending on the value.

Alternatively, if you use one plural regardless of difficulty and have your Single scripts be different depending on the difficulty, in TPlural handle which Singles to load depending on the CommonData. After all, once set, it can be accessed from any other running script in the session.

A more advanced technique uses one Single for all difficulties. Earlier, I mentioned the utility of the string to integer mapping. This allows us to directly use the difficulty value in bullet patterns (e.g. loop(15 + GetCommonData("Difficulty", 0) * 3) { CreateShotA1(...); } in addition to other less restrictive tasks such as choosing plural scripts and the like.

EXERCISE: Lock the player's position and disable player shooting in addition to disabling player spellcards/bombs.

Quiz: Common Data

1) One of the aspects of difficulties that was not discussed above was displaying the difficulty in the top right corner of the STG_Frame. To do this, we generally utilize the System script. A key component of this implementation involves displaying no text before a difficulty has been selected. Which blocking code snippet best implements this requirement in accordance with standards for default variables?

A. while (GetCommonData("Difficulty", 0) == 0) {yield;}
B. if (GetCommonData("Difficulty", 0) == 0) {yield;}
C. while (GetCommonData("Difficulty", -1) == -1) {yield;}
D. if (GetCommonData("Difficulty", -1) == -1) {yield;}

2) Now, let's say that we want to have the difficulty text in the top right display the currently selected difficulty during the menu as opposed to only after a difficulty has been selected. Assuming we use an infinite loop in the Difficulty text control in the System script that changes the text, color, etc. based on the current CommonData, what can we add to our current Difficulty select menu to dynamically change the difficulty?

Hit 'Show' to show possible answers.

Suggested answer is below.

    //Infinite Loop that runs until the player has made a selection.
    loop{
        //Menu controls
        if(GetVirtualKeyState(VK_UP) == KEY_PUSH){selection--;}
        if(GetVirtualKeyState(VK_DOWN) == KEY_PUSH){selection++;}
        if(selection < 0){selection = 1;}
        if(selection > 1){selection = 0;}

        //Make it clear to the player which option is selected
        ascent(i in 0..length(TextObjects)){
            if(selection == i){
                ObjText_SetFontColorTop(TextObjects[i], r[i], g[i], b[i]);
                ObjText_SetFontColorBottom(TextObjects[i], r[i]/1.5, g[i]/1.5, b[i]/1.5);
            } else {
                ObjText_SetFontColorTop(TextObjects[i], r[i]/1.75, g[i]/1.75, b[i]/1.75);
                ObjText_SetFontColorBottom(TextObjects[i], r[i]/2.25, g[i]/2.25, b[i]/2.25);
            }
        }

        //Handle setting Common Data here
        if(GetVirtualKeyState(VK_SHOT) == KEY_PUSH){
            sdiff = diffs[selection];
            SetCommonData("Difficulty", sdiff); //Set the CommonData here
            break;
        }

        if(selection == 0){SetCommonData("Difficulty", 1);}
        if(selection == 1){SetCommonData("Difficulty", 2);}
	
        yield;
    }

Part 5: How do I communicate between scripts?

To close off this lesson, we will have a brief discussion on other uses for standard Common Data.

One rather advanced usage of Common Data is communication between the Player script and the stage being played. Let's say that we have a system where every time a spellcard is captured, points are accumulated that allow for the player to release a special attack, after which the points are depleted.

As will be discussed in a later lesson, anything involving player shots should be handled inside a player script. Therefore, in TFinalize for our spellcards, we will increment the number of points. In the player script, on special attack toggle, the player script first checks to see if enough points have been accrued. If so, launch the attack and consume the points. And finally, the System script checks the number of points each frame to display it on the HUD so that the player can see how many points they currently have.

Systems like these become possible with Common Data. In addition to the above, in-stage menus to set Common Data can be used for effects (so the player can choose how heavy the effects in the stage are), various gameplay modes (Pointdevice vs Legacy, Infinite Lives toggle, etc.), and more.

Depending on what you need, it may be possible to implement your ideas without CommonData (or Events). But they are powerful tools that will enable many exciting and interesting features for your players.

As a final note, there are many ways to use Common Data and the examples shown in this lesson are just a few. I suggest experimenting on your own in order to determine what works best for you.

Summary

  • CommonData can be used to share values between scripts
  • CommonData can be used to set difficulty and other configuration settings for the duration of a script

Sources and External Resources

N/A