Site Logo


Sparen's Danmakufu ph3 Tutorials Lesson 18 - Creating and Using Functions and Libraries

The video for this lesson is Ephy/Lefkada's Letty X Lily script, an entry for the Contradictory Catastrophe contest.

Part 1: What will be Covered in this Lesson?

In this lesson, I will discuss in great detail functions, subroutines, and function libraries. This lesson assumes that you have read and are familiar with the contents of Lesson 5. Lesson 15 is also recommended reading.

Part 2: What are Functions?

Back in Lesson 5, we introduced functions. In Danmakufu ph3, all functions have names - there are built-in functions and of course, user-defined functions. Built-in functions such as GetCurrentScriptDirectory are in CamelCase (PascalCase, to be exact). User defined functions can be literally anything that isn't a reserved keyword.

Functions may take parameters - they're optional but may be useful depending on the usage of the function. Functions may also return values or simply return nothing. They are also executed in the main thread - the next chunk of code will not execute until the function has finished execution. However, since yield; is rarely used inside functions, this is not a major problem and will go unnoticed for the most part.

As a reminder, in this tutorial (and in many other resources for general computer science), functions are addressed by functionname(), where the parentheses hold any parameters the function may have.

Part 3: What are the Differences between Functions, Tasks, and Subroutines?

We've gone over functions and tasks before, but we will now discuss tasks in the context of libraries. We will also have a brief introduction to subroutines, which are not generally useful until you begin larger scale projects.

As stated in Lesson 5, tasks, or coroutines, are similar to functions except that they run in parallel to the @MainLoop(s) and each other. Tasks are good for organizing code by taking large chunks of code and labeling them with descriptive names, and they are useful in script flow and control. Tasks are used for a large variety of things, from timing sound effects to repeating code while the boss is alive, etc.

Tasks cannot return values, and can be broken out of using return;, which will immediately stop execution of a task. Since they run in parallel (tasks run after @MainLoop has run due to the yield; in @MainLoop), they can access changing values with scope that is global with respect to the task, and can respond accordingly.

And now we reach subroutines. Subroutines are created in a similar way to functions and tasks - you say sub name {} and voila. However, subroutines have a number of limitations - they cannot take parameters and, like functions, they run in series with the code block that called them, effectively making them functions with less power. However, their strength lies in code organization, as you can store commonly used code in subroutines to reduce code redundancy (though this is the same of tasks and functions).

Part 4: What are Function Libraries?

So now that we have a brief understanding of functions, tasks, and subroutines, it's time that we looked at function libraries and #includes.

Back in lesson 5 Part 7, we created our first function.

function wait(n){loop(n){yield;}}

This function ended up being shoved in the end of every script we wrote in Lesson 7, along with some other user-defined functions we made. However, this meant that if we had 100 attacks done the same way, we'd have 100 copies of each of these functions! And what would happen if we made a mistake in one of them but realized after we had already made dozens of different attacks? Well, we'd have to go back and individually change each and every one of them.

This is where libraries come in handy. You can use one library and define the functions in one place, and then 'include' those libraries in multiple scripts. Libraries are not the only things that can be included. We've included Shot Constant sheets, for example. And you can put pretty much anything in a file and include that file elsewhere. We'll get into some of the quirks in the next part, but for now, let's do some examples.

First, let's create a directory called 'lib' in one of your script folders. (If you do not yet do so, I suggest having separate folders for music, sound effects, scripts, and images as well). Inside this 'lib' folder, let's create 'lib_general.dnh'. And inside that file, let's paste the following:

function GetCenterX(){
    return GetStgFrameWidth() / 2;
}
function GetCenterY(){
    return GetStgFrameHeight() / 2;
}
function wait(n){
     loop(n){yield;}
}

You may recognize these three functions - in fact, you definitely should since you should have been using them.

Now, let's say you have a Single script called boss_ns1.txt, located in the same directory as lib. Now, after the Danmakufu Header, let's include the following:

#include "./lib/lib_general.dnh"

You do not need a semicolon at the end of the line. You do not need a space between #include and the first quotation mark - in other words:

#include"./lib/lib_general.dnh" //This is also valid but is not preferred

Now, if wait(), GetCenterX(), and GetCenterY() have already been defined in the Single script, running it will bring up an error about functions already existing with the same name. This is because when we #include lib_general.dnh, the three functions have been defined, but when Danmakufu reaches the end of the Single script, it find duplicates. Remove the duplicated functions (the versions in the Single script).

Congratulations! You have created and included your first function library!

Note: tasks are often grouped together in libraries as well. Additionally, it is better to have more smaller functions/tasks that do simpler jobs than to have gigantic functions/tasks that do numerous things.

EXERCISE: If you have yet to actually move general purpose functions such as wait() into a library, go and do it now.

Part 5: How Do I Use #include?

Most of the time, you will want to include multiple libraries in a single script. For example, you may have a cutin library for spellcard cutins. So now it's a good idea to elaborate on the peculiarities of #include.

Firstly, pathing. When using #include, you move forwards and backwards in the file system using standard pathing notation, but you CANNOT use functions such as GetCurrentScriptDirectory() in the #include line - you use './' instead to point to the current directory. In the same way, to go back a directory, you use the standard '../'. So if boss_ns1.txt was located in a script directory in the same folder as lib, we would have used the following:

#include "./../lib/lib_general.dnh"

Next is the issue of multiple includes in the same file. We will discuss dependencies in the next part of this lesson.

As shown above, we can easily have conflicts between libraries. But what if two libraries separately define GetCenterX() or something similar? Well, if the functions defined individually are the same, the best option is to first include your general purpose function library, and then include the more specialized library. In this case, the more specialized library's copy of the function will be removed. To be more specific, the order of #includes in a script determines which included files have access to contents of other included files. If you do this:

#include "./../lib/lib_general.dnh"
#include "./../lib/lib_cutin.dnh"

...then lib_cutin will be able to access the contents of lib_general. HOWEVER, lib_general will NOT be able to access the contents of lib_cutin. This is in contrast to other programming languages, and there are many more peculiarities with this system as well. For example, accessing global variables located in a script in an included file. In general, simply do not do this. Libraries should not depend on variables defined inside a script. If you want to get the boss scene within a library, use GetEnemyBossSceneObjectID() within the library instead of referencing the objScene variable in the script. Want to get the boss HP? Pass the boss object as a parameter to the relevant function in the library, like we did for BulletCommand tasks in earlier lessons. For example:

/* *************************************************************************
*BulletDeleteEffect(obj) -- Creates a shot deletion effect
*Param: objparent (obj) - Shot Object to render effect at
************************************************************************* */
task BulletDeleteEffect(objparent){
    let path = GetCurrentScriptDirectory~"./img/bulletdelete.png";
    let obj = ObjPrim_Create(OBJ_SPRITE_2D);
    ObjPrim_SetTexture(obj, path);
    ObjRender_SetPosition(obj, ObjMove_GetX(objparent), ObjMove_GetY(objparent), 1);
    ObjRender_SetBlendType(obj, BLEND_ADD_ARGB);
    ObjRender_SetScaleXYZ(obj, 0.5, 0.5, 0);
    if(ObjShot_GetImageID(objparent) <= 0){return;}
    let shotinfo = GetShotDataInfoA1(ObjShot_GetImageID(objparent), TARGET_ENEMY, INFO_DELAY_COLOR);
    ObjRender_SetColor(obj, shotinfo[0], shotinfo[1], shotinfo[2]);
    ascent(i in 0..8){
        ObjSprite2D_SetSourceRect(obj, i*64, 0, (i + 1)*64, 64);
        ObjSprite2D_SetDestCenter(obj);
        loop(5){yield;}
    }
    Obj_Delete(obj);
}

The above is a sample Bullet Delete Effect task, which is located within a function library. Note that it takes a parameter objparent (a Shot Object in this case), where this parameter is used for things such as object position. Although it may seem obvious to do this for a shot object, which can be any object, the same applies for enemy objects and boss objects - if you were to reference the enemy boss within a function library, there would be problems if there was no boss, for example.

To close this part of the lesson, we will conclude with the most painful part of #include files - the usage of GetCurrentScriptDirectory() WITHIN an included file. In 0.12m, it would use the path of the script that included the #include file. In ph3 however, GetCurrentScriptDirectory() uses the path to the included file. In other words, if you wanted to reference an image event_reimuhappy.png in the img folder from lib_dialogue.dnh located in the lib directory, where lib_dialogue.dnh was included by a Stage script in the same directory as both lib and img, you would use the following code:

//In the Stage script
#include "./lib/lib_dialogue.dnh"

//In the Dialogue Library
let pathReimu = GetCurrentScriptDirectory() ~ "../img/event_reimuhappy.png";

And that about covers the absolute basics for non-nested include statements.

Part 6: How do #include statements within #included files work?

Once you start building more and more libraries, it becomes more and more of a pain to manually include every file individually. Additionally, packages do not allow you to use certain object types and functions on those types are undefined. You will be forced to create separate libraries when you build larger projects, both to enhance readability (and to make it easier to find things) and to separate what is used where.

One way to deal with all of these problems is to have certain function libraries #include other libraries as dependencies. However, this has its fair share of problems, which I will get into.

What is a dependency, though? Simply put, it's a library that is required for other libraries to function. In other words, if a dialogue library requires GetCenterY(), and lib_general.dnh contains that function, lib_general.dnh can be considered a dependency of the dialogue library. Anyways, #including files in #included files.

First the benefits: If we have several function libraries that depend on the GetCenterX() function located in lib_general.dnh, for example. One method is, in the script using the libraries, to first #include lib_general.dnh and then #include the other libraries. But once you have 5 lines of include statements, this becomes problematic, especially if you decide to change one of the file names - you have to go through every place that #includes that file and manually fix the filenames.

Now, alternatively you could create a file fxn_include.dnh that contains the following:

#include "./fxn_general.dnh"
#include "./fxn_library1.dnh"
...

Using the above, you could include one file - fxn_include.dnh - in your scripts in place of all of the individual #include statements. This makes it easier to debug when something goes wrong, and makes dependencies much easier in that the order in which you #include in the file is equivalent to the loading order, just as before. By keeping it all in one place, adding a dependency won't require you to change the order of your #include statements in every single script you use them in.

However, there are the disadvantages - firstly that you can't access any global variables from a twice #included file and secondly that you will still need to differentiate between libraries that are #included in packages and libraries that can be #included anywhere. Dependencies may become problematic in this case.

Still, if you have many different libraries, having files specifically to #include other files is good for organization and, if all the functions and tasks in your libraries are self-sufficient in terms of variables, it should work smoothly.

Quiz: #include

1) Parsee is extremely jealous of Yamame's new function libraries, each of which contains parameterized tasks unique to specific ailments. Therefore, Parsee decides to take action and messes with something. Yamame returns the next day to find that although all the libraries are there and all the paths are correct, the program is pausing every time a new disease is called. What is the issue here?

A. The incorrect files were #included
B. All of the tasks were changed to subroutines
C. All of the tasks were changed to functions
D. Danmakufu is trying to load new boss scene objects

2) Yuugi is trying to include a file located in the lib directory, but Danmakufu crashes. Code is below.

#include GetCurrentScriptDirectory() ~ "lib/fxn_general.dnh"

What is her issue?

A. She should use './' instead of GetCurrentScriptDirectory()
B. GetCurrentScriptDirectory() should be in quotes
C. There should not be a space between #include and GetCurrentScriptDirectory()
D. She should use '../' instead of GetCurrentScriptDirectory()

Part 7: What is Documentation and Why Should (or Shouldn't) I Use It?

To close this lesson, I would like to give some brief notes on Documentation and why you should (or should not) use it. Documentation is basically a way of explaining what your code does, but not like with simple single-line comments. Documentation is a standard way of telling users of your code (including yourself) what your code does, what it returns, etc. The Danmakufu Wiki's function list contains Documentation of all the functions in the language and what they do. Of course, you will want local documentation within your libraries themselves.

So why would you perhaps not want to use it? Well, if you want to render your code incomprehensible to everyone else (and perhaps yourself in the future), then not having documentation is fine. If you code is 'self-documenting' (i.e. the names of your functions explain what they do), that's... frowned upon but done all the time in actual programming.

Anyways, having documentation is a good idea. So let's get some examples.

First, my sample sound library - Link here

As you can see, the top of the file has a large block comment. This block contains information about the author of the library as well as other relevant information (licenses, which version of a program it is used for, compatibility, dependencies, etc). Usage instructions have also been provided for this particular library.

After this, before each function, we have a block comment with some contents. For example:

/* *************************************************************************
*obj SoundSFX(string) -- Loads a sound effect and creates a sound object for the given file path
*Param: path (string) - File Path of sound file to use
*Return: Object ID of created Sound Object, or null if invalid path
************************************************************************* */

The first line of text contains a few things of note. First, the obj SoundSFX(string) portion. In many languages, it is customary to label functions this way - return type, then the function name with parameter types in parenthesis. After this there is a description of what the function does.

After this, I have two lines for Param (parameters) and Return (return value), respectively. As before, Param has the name of a parameter followed by its type in parenthesis and then a description of the type. Return also has a description of the return value. Note that it was not obvious what the function did by the name and that it was also not obvious what the return value was. Without the documentation, it would be difficult for someone seeing the code for the first time to understand what the code did.

Below is another example.

/* ******************************************************************
* Sparen's ObjMessageLog Library
* For use with Touhou Danmakufu ph3
* (C) Sparen of Iria 2015
****************************************************************** */

/* ******************************************************************
* ObjMessageLog - An object that stores all message data and assists with writing it to the text box
* Extends: ObjSprite2D
* Extended by: N/A
* Dependencies: lib_Text
****************************************************************** */

/* ******************************************************************
* Constructors (ObjMessageLog)
****************************************************************** */

//Initializes a Message Log Object.
//Param: N/A
//Return: Object ID (int)
function ObjMessageLog_Create(){
    let obj = ObjPrim_Create(OBJ_SPRITE_2D);
    Obj_SetValue(obj, "mlog_lines", 0);
    Obj_SetValue(obj, "mlog_log", []);

    ObjMessageLog_Manage(obj);
    ObjMessageLog_Render(obj);

    return obj;
}

In this example, we once again have a block comment at the top with some basic information as well as another block comment describing dependencies. This is a custom render object class in Danmakufu, so it extends ObjSprite2D, and this information is clearly stated at top if the file.

As for the first method, there is once again a description of what the function does, any possible parameters, and the return value.

In the end, what kind of documentation you use does not really matter as long as it is consistent across a library and contains all the necessary information for someone to use your library without problems.

Summary

  • When a statement is called involving a function, the function will complete execution before the outer statement can continue.
  • Libraries contain related functions and/or tasks in a single file
  • Good libraries do not depend on external variables
  • #include is used to utilize libraries and other text-based code assets. You cannot use functions inside of it.
  • #include can be used within #included files
  • The order in which files are #included matters if one requires functions defined in the other
  • Documentation can be used to describe what exactly a function or task does.

Sources and External Resources

N/A