The video for this lesson is Vigor's Mus Armus for the RaNGE 16 contest - a script that shows off his custom STG_Frame and lifebars.
In this lesson, I will discuss System scripts, which are run for the duration of any single, plural, or stage. They contain things that must be run across entire scripts such as the STG_Frame, contents of the HUD (player score, life, etc), and custom scripts. They are also excellent places to load fonts and shotsheets, as well as link Danmakufu .dat archives.
This lesson requires knowledge of render and text objects and will also contain brief information on extend systems and the like, which will be revisited after CommonData has been introduced. For reference, please see Lesson 13 and Lesson 17 for 2D Sprites and Text, respectively.
So, System Scripts. These scripts, linked to via #System in the Danmakufu Header, are executed when you begin the execution of a script. By default, if there is no #System specified in a script, Danmakufu will default to the system file provided in default_system. It is imperative that you do not mess around with these files and do not link to them using #System.
We will begin by examining the existing contents of Default_System.txt in the context of making our own System script. Therefore, we will make a new directory called system
in any script folder (so we will have img, lib, system, etc) and will copy all of the contents of default_system into that folder. You can leave out bgm - we won't be using it.
Now, let's take your plural script, and in the Danmakufu header, add #System["path/to/system/file"]
with the relative path from the plural to Default_System.txt in the quotes. For example, #System["./../system/Default_System.txt"]
. This will force the plural to always load the system script in /system/ rather than the default. It is highly recommended that you also add #System to all of your Single scripts so that when you run them individually, the correct system script is loaded.
Now that we have that done, let's take a look at the contents of Default_System.txt, and let's do some changes and edits.
The first thing you will notice is that the system file is long - @Initialize calls a large number of tasks. Each of these handles a specific part of the HUD, such as the STG_Frame, Score, or the like.
Let's begin with the first function called - InitFrame()
.
function InitFrame()
{
let path = GetCurrentScriptDirectory() ~ "img/Default_SystemBackground.png";
let obj = ObjPrim_Create(OBJ_SPRITE_2D);
ObjPrim_SetTexture(obj, path);
Obj_SetRenderPriority(obj, 0);
ObjSprite2D_SetSourceRect(obj, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
ObjSprite2D_SetDestRect(obj, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
}
What InitFrame() does is simple - it takes the STG_Frame graphic (see the file in question to get a better sense of what it is) and pastes it on the screen with the lowest render priority possible - 0. Since 0 is less than 0.2, the position is relative to the top left corner of the window. The assigned rects are SCREEN_WIDTH and SCREEN_HEIGHT, or 640 and 480 respectively (depending on what was set in your def file, if you use one). This pastes the entire thing neatly on the screen. The playing field is black in the STG_Frame - both black and transparent are perfectly fine for this.
Note that SCREEN_WIDTH and SCREEN_HEIGHT are predefined constants locked to 640 and 480. If you have changed the window size of Danmakufu using th_dnh.def, consider GetScreenWidth()
and GetScreenHeight()
.
Besides changing the image, it is possible to change the size of the playing field as well. To do this, you will use the SetStgFrame()
function, whose default values are (32, 16, 416, 464, 20, 80). The first four arguments are the left, top, right, and bottom of the playing field with respect to the top left corner of the window. The last two are the render priorities used as the boundaries for what is rendered with respect to the top left of the playing field. By default, render priorities between 20 and 80 will use the top left corner of the playing field as (0,0) and all others will use the top left corner of the window.
It is highly suggested that you put SetStgFrame()
in @Initialize of your system script.
Now we will discuss all of the various fields that are on the screen - score, lives, bombs, and graze. We will also go over how to add more of these.
Looking at the defaults, it becomes quite obvious that there is a text object for the words and a sprite list for the numbers. The text object is obviously customizable, and you can move it wherever you want. The text object can be substituted for a sprite if you want fancier effects.
The sprite list can be substituted for text if you want to use your own font, or you can change the font in Default_SystemDigit.png to give custom pixel numbers. This is entirely up to you. Do note, however, that with a sprite list, you are manually positioning each individual sprite (each individual number). It is recommended that you study the examples in Default_System.txt if you want to continue using sprite lists for information such as score.
But all of this is besides the point - the tasks that control the displays - TScore, TGraze, and so on, run for the entire duration of your script, terminating upon script completion. They all use a while loop for updating and a series of functions to obtain their data.
GetScore()
, GetGraze()
, GetPlayerLife()
, and GetPlayerSpell()
are all aptly named and return exactly what you think they will return. For the latter two, they return the amount remaining. In other words, if GetPlayerLife()
returns 0, the player is still alive but has no lives remaining.
So let's do some tweaking. To start, let's convert the sprite list to a text object.
In the default system file, the following is used:
let count = 2;
...
let point = GetPlayerSpell();
point = min(point, 99);
let listNum = DigitToArray(point, count);
What this does is simple - count is the number of digits to show (in the case of player bombs, 2), and min(point, 99)
makes sure that point never exceeds 99 on the display.
For a text object, we can use rtos()
instead of count and DigitToArray. What rtos()
does is take a number and use the given format string to turn the number as a string. For example:
let score = GetScore();
score = min(score, 9999999999999);
ObjText_SetText(objNum, rtos("0000000000000", score));
First, we get the score, and then we make sure the score to display is not greater than 9999999999999. Then we use rtos. If you take note of the number of 0s, it is the same as the number of 9s. Therefore, this ensures that our score will be rendered the way we want it. See rtos to see the formatters on the string.
If we did rtos("00000", 45)
, we would get "00045". Using the '.' is required if you want decimal points to be recognized.
So using rtos, we can turn our number into a string and use a text object to display it. Of course, we are limited by the general limitations of text objects. For a more concrete example:
task TGraze(){
let objGraze = ObjText_Create();
ObjText_SetText(objGraze, "Graze");
ObjText_SetFontSize(objGraze, 20);
ObjText_SetFontType(objGraze, "Fairview Regular");
ObjText_SetFontBold(objGraze, false);
ObjText_SetFontColorTop(objGraze, 255, 255, 255);
ObjText_SetFontColorBottom(objGraze, 255, 255, 255);
ObjText_SetFontBorderType(objGraze, BORDER_FULL);
ObjText_SetFontBorderColor(objGraze, 255, 128, 128);
ObjText_SetFontBorderWidth(objGraze, 2);
Obj_SetRenderPriority(objGraze, 0.01);
ObjRender_SetX(objGraze, 425);
ObjRender_SetY(objGraze, 199);
let objNum = ObjText_Create();
ObjText_SetText(objNum, "000000");
ObjText_SetFontSize(objNum, 20);
ObjText_SetFontType(objNum, "Helvetica");
ObjText_SetFontBold(objNum, true);
ObjText_SetFontColorTop(objNum, 255, 255, 255);
ObjText_SetFontColorBottom(objNum, 255, 255, 255);
ObjText_SetFontBorderType(objNum, BORDER_FULL);
ObjText_SetFontBorderColor(objNum, 0, 0, 0);
ObjText_SetFontBorderWidth(objNum, 2);
Obj_SetRenderPriority(objNum, 0.1);
ObjRender_SetX(objNum, 528);
ObjRender_SetY(objNum, 201);
while(true){
let score = GetGraze();
score = min(score, 999999);
ObjText_SetText(objNum, rtos("000000", score));
yield;
}
}
Note that depending on your fonts, you may have to do some additional tweaks to align your text and numbers correctly, especially when using two different fonts for text and numbers.
When using CommonData to store custom fields (e.g. high score, etc), you can use the same method as above to display them on the HUD.
Now, it is also popular to use graphics for life and bombs. In this case, I recommend using an array of sprites or a sprite list. How you implement this is up to you, but determining which graphic to use is the hardest part. If you don't have life and bomb fragments, you can actually make one set of sprites to hold the 'empty' graphic (if you decide to use them at all), and have another set of sprites to hold the 'full' graphic, and toggle Obj_SetVisible()
to determine whether or not to display the 'full' graphic.
I won't provide code here, but you can try it out for yourself. Just as a note, you will need to use CommonData in order to handle life fragments or bomb fragments. This of course will have a thorough explanation in a later tutorial.
System Scripts are used across an entire script - usually, the same system script is linked in every single script in a project. Therefore, numerous things that must run regardless of the script are handled here - including but not limited to preloading shotsheets, custom item scripts, and the big three: Pause, EndScene, and ReplaySaveScene scripts.
You will want to replace these or at the very least modify them. To start, the Pause and EndScene scripts are in Japanese, and the ReplaySaveScreen has a number of annoying flaws. Additionally, you will probably want to change the fonts and colors.
We will go over how to modify these scripts in a later tutorial, but linking your new ones is quite simple.
Basically, go to @Initialize, and add the following:
SetPauseScriptPath(GetCurrentScriptDirectory() ~ "./Pause.dnh");
SetEndSceneScriptPath(GetCurrentScriptDirectory() ~ "./EndScene.dnh");
SetReplaySaveSceneScriptPath(GetCurrentScriptDirectory() ~ "./ReplaySaveScene.dnh");
These three functions tell Danmakufu to use your custom Pause, EndScene, and ReplaySaveScene scripts instead of the copies found in default_system. I highly recommend using these functions and linking to a local set of the three scripts, even if you don't do any heavy edits. As a general rule of thumb, don't edit default_system - have a local system folder in each script and link to that instead.
System Scripts are often used to control a number of things such as the boss lifebar, boss timer, and he like. The @Event of a system script also handles what happens when you start and capture a spellcard using the events EV_START_BOSS_SPELL
and EV_GAIN_SPELL
. Though you are free to put these in any @Event anywhere, they are usually consistent throughout the entire script and as a result, it is highly recommended that you use them in your system script.
EV_START_BOSS_SPELL runs at the start of a spellcard. It is commonly used to play sound effects at the start of all spells and for ant effects not in the cutin library. EV_GAIN_SPELL is commonly used to play sound effects upon spell capture and perform any other things done at that time such as displaying the spellcard bonus. In a much earlier tutorial, we awarded spellcard bonuses using TFinalize. Handling it in this event in the system script is highly recommended, especially if you are copy-pasting the spellcard bonus award code in each script.
It is also common to have custom events in System scripts, but we will get into that at a much later time. Note: The default spellcard sound effect is not handled in the default system file, but in Default_System_MagicCircle.txt. If you want to change the default spell sound effect, I suggest using EV_START_BOSS_SPELL in the system file and removing the sound data from your local Magic Circle.
1) Eirin comes up with a clever way to handle life fragments - she simply uses decimal points to handle them. Assuming that there are 5 fragments per life (0.2 lives per fragment), how would she handle a text object stating the amount of fragments needed to reach the next extend?
Hit 'Show' to show possible answers.
Suggested answer is below.
...
// Assume objNum2 is the text object used to show the number of fragments needed
while(true){
let lives = GetPlayerLife();
lives = min(score, 9);
lives = max(score, 0);
ObjText_SetText(objNum2, "Next: " ~ rtos("0", 5 - 5*(lives - (truncate(lives)))));
yield;
}
2) Write code to handle awarding of the spellcard bonus. Please set the last digit to 0 (as per convention). Only write code for the actual awarding of the score - not the display of the score on the screen.
Hit 'Show' to show possible answers.
Suggested answer is below.
alternative(GetEventType())
case(EV_START_BOSS_SPELL){
...
}case(EV_GAIN_SPELL){
let objScene = GetEnemyBossSceneObjectID();
let score = truncate(ObjEnemyBossScene_GetInfo(objScene, INFO_SPELL_SCORE) / 10) * 10;
if(ObjEnemyBossScene_GetInfo(objScene, INFO_PLAYER_SHOOTDOWN_COUNT) + ObjEnemyBossScene_GetInfo(objScene, INFO_PLAYER_SPELL_COUNT) == 0){
AddScore(score);
}
N/A