ZScript_Basics

🟢 «< BACK TO START

🔵 « Previous: Pointers and casting 🔵 » Next: Virtual Functions


Custom functions and function types

Defining custom functions

Sometimes you need to perform a bunch of similar actions in multiple places and/or multiple actors. Usually you can simplify things by creating a custom function. Most functions will look like this:

//pseudocode:
type name (arguments) 
{
    //what the function does
}

Functions have to be defined inside a class, and by default only that class will be able to call them—this type of function is called a method. The only type of non-method functions is anonymous functions.

Let’s revisit our CacoSingleDad for an actual example:

class CacoSingleDad : Cacodemon replaces Cacodemon 
{
    Actor baby;

    void SpawnBaby()        //defines a function that can be used by CacoSingleDad
    {
        baby = Spawn("Cacodemon",pos,NO_REPLACE);
        if (baby) 
        {
            baby.Warp(self,64,0,0);
            baby.A_SetHealth(800);
            baby.A_SetSize(16,30);
            baby.speed = 12;
            baby.floatspeed = 6;
            baby.A_SetScale(0.5);
            baby.A_SetTranslation("BabyCalm");
        }
    }

    void AngerBaby() 
    {
        if (baby && baby.health > 0)
        {
            baby.A_StartSound("caco/active");
            baby.A_SetTranslation("BabyAngry");
            baby.speed *= 2; 
            baby.floatspeed*= 1.5;
            baby.bNOPAIN = true;
        }
        // Note, if the 'baby' pointer is null,
        // or its health is 0 or less, this
        // function simply won't do anything.
    }

    States 
    {
    Spawn:
        TNT1 A 0 NoDelay SpawnBaby();
        HEAD A 10 A_Look;
        wait;
    Death:
        TNT1 A 0 AngerBaby();
        goto super::Death;
    }
}

The code should hopefully be simple enough to understand: we defined two functions, SpawnBaby and AngerBaby. The first one spawns the baby Caco and performs all the stuff we need to do (sets color, health, speed, etc.), and the second one checks if baby exists and is alive, and if so, does the other stuff.

There are two characteristics of this function to consider:

As a result, this function isn’t very flexible, and most cases you’ll need to create something more complex.

For example, let’s say we want to create a universal “baby-spawning function” that allows us to manually specify the baby’s class and health. For that we need to declare variables that will serve as arguments and then use them within a function:

//defining the function:
void SpawnBabyExtended(Class<Actor> spawnclass, int babyhealth = 100, int babyspeed = 10, double babyscale = 0.5) 
{
    baby = Spawn(spawnclass,pos,NO_REPLACE);
    if (baby) 
    {
        baby.Warp(self,64,0,0);
        baby.A_SetHealth(babyhealth);
        baby.speed = babyspeed;
        baby.A_SetScale(babyscale);
        baby.A_SetTranslation("BabyCalm");
    }
}

//using the function:
TNT1 A 0 NoDelay SpawnBabyExtended("Cacodemon",800,12);

The arguments are defined in the same way as variables:

type name = defaultValue

If you provide a value, this will be the default value for the corresponding argument. For example, int babyspeed = 10 defines babyspeed as an optional argument: if you don’t set a value when calling this function, then the default value will be used (which in the example above is 10).

At the same time, spawnclass is not an optional argument: when using the function, you have to provide a name of the actor to spawn; there’s no “default” actor that would spawn instead.

The arguments you defined can be used within a function as shown in the example, e.g. baby.A_SetHealth(babyhealth); will make the spawned actor’s health equal to the number you provide when calling the function (in the example it’s 800). We also provided a default value of 100 for the argument.

We could make this function a bit more universal by improving how it treats the default values:

void SpawnBabyUniversal(Class<Actor> spawnclass, int babyhealth = 0, int babyspeed = 0, double babyscale = 0) 
{
    baby = Spawn(spawnclass,pos,NO_REPLACE);
    if (baby) 
    {
        baby.Warp(self,64,0,0);
        if (babyhealth != 0)            //executes if 'babyhealth' is not equal to 0
            baby.A_SetHealth(babyhealth);
        if (babyspeed != 0)                //executes if 'babyspeed' is not equal to 0
            baby.speed = babyspeed;
        if (babyscale != 0)                //executes if 'babyscale' is not equal to 0
            baby.A_SetScale(babyscale);
        baby.A_SetTranslation("BabyCalm");
    }
}

In this version of the function the default values are 0 and they’re treated as an instruction to use default values for the actor: unless you specify values for health, speed and scale explicitly, the spawned actor will simply use its own default values (as provided in its Default block). So, for example, if you spawn a Mancubus instead of a Cacodemon, it’ll have Mancubus’ default health value; same goes for scale and speed.

It’s up to you which arguments to make optional and which to leave compulsory (in SpawnBabyUniversal() above the only non-optional argument is spawnclass—the type of the actor to spawn); the only rule to remember is that non-optional arguments should be defined first—you can’t have non-optional arguments defined after optional ones.

You can notice that this function doesn’t actually cover everything we used in CacoSingleDad originally (such as modifying the baby’s floatspeed), but you can easily expand it. It will also only work on actors that actually have a baby variable defined—so, if you want to create multiple custom functions, it may be a good idea to put them all inside your own version of the base Actor class and have your classes inherit from it.

A note on function names: you can give your functions any names you like. It’s considered somewhat traditional to use the A_ prefix for action functions (a later section of this chapter explains what these are), and not use it otherwise. You may also use a custom prefix, like the abbreviation for your mod. Just make sure the name is sensible and not overly long.

Non-void functions and return values

Void functions are the functions that do stuff and don’t return any data. But there are many cases when you need to retrieve some sort of data using a function. Here’s a very basic example:

int GetTargetHealth() 
{
    if (target)
    {
        return target.health;
    }
    else
    {
        return 0; //we have to return something, so let's use 0 as default
    }
}

This function checks if the actor that called it has a target, and if so, returns the amount of health it has. If there’s no target, it doesn’t return anything.

Note that you have to include a null-check here, as well as cover all possible cases: there has to be a return value for the case where target doesn’t exist, that’s why we must provide return 0. If we don’t, the function won’t know what to do. (Nothing terrible will happen, GZDoom simply won’t start. Bad stuff will happen if you skip the null-check, however, since it can cause a VM abort due to a null pointer).

It can also be slightly simplified:

int GetTargetHealth() 
{
    if (target)
    {
        return target.health;
    }
    return 0;
}

We don’t need to use else here because the function cuts off at the point where you use return; so, if the target exists, the function will return its health and immediately stop doing anything else.

Function types are the same as variable types: they can hold numeric values, pointers, coordinates, etc. Here’s an example of a custom version of A_MonsterRefire that is a state function:

class ChaingunGuyWithAMagazine : ChaingunGuy 
{
    int monstermag;    //this variable holds the number of "ammo" in monster's magazine
    Property monstermag : monstermag;

    Default 
    {
        ChaingunGuyWithAMagazine.monstermag 40;
    }

    state CustomMonsterRefire(int ChanceToEnd = 0, statelabel endstate = "See") 
    { 
        if (monstermag <= 0)                  //check how much "ammo" is left
        {
            return ResolveState("Reload");      //if 0, goto Reload state
        }
        else if (ChanceToEnd > random(0,100))  //otherwise check ChanceToEnd against a random value
        {
            return ResolveState(endstate);      //if true, go to end state
        }
        return null;                         //otherwise don't do anything
    }

    States 
    {
    Missile:
        CPAS F 2 
        {
                A_CPosAttack();
                monstermag--;
        }
        CPAS E 2 A_FaceTarget;
        CPAS F 2 A_CPosAttack;
        CPAS E 2 A_FaceTarget();
        TNT1 A 0 CustomMonsterRefire(5,"AttackEnd");
        loop;
    AttackEnd:
        CPAS A 20;
        goto See;
    Reload:
        CPAS A 40 
        {
            monstermag = 40;
        }
        goto See;
    }
}

This function is designed for monsters that have “magazines” and need to reload their weapons. The monster in question doesn’t use actual items for ammo, instead there’s a variable that holds how much ammo it has.

This function works as a state jump, such as A_Jump: when the function is called in a monster’s state, it tells the state machine where to go. Specifically, it does the following:

Action functions

Action functions are functions that have the keyword action added in front of the function type. For example:

action void MyFunction()
{
    [...] //function contents
}

The concept of an action function is specific to ZDoom and can be confusing to explain. On the technical side, when a function is defined as an action function, it gains access to a special struct FStateParamInfo, which allows the function to know exactly which state it’s called from and what called it. Regular actors have no reason to be confused about it, but there’s one special case: classes that can draw stuff on player’s screen. Currently only two classes can do that: Weapon and CustomInventory.

Both of those classes don’t use their own states directly; instead a separate internal class PSprite handles drawing on-screen sprites. What’s more, a weapon has to interact with both data defined in the weapon (such as its flags, ammotypes, etc.) and the data defined on its owner (such as checking the PlayerPawn’s inventory for remaining ammo, getting its position, angle and pitch to determine the direction of the shooting, and so on).

To be able to interact with that data properly, weapons interact with two pointers: self — which is not the weapon, but the PlayerPawn owner — and invoker — which refers to the weapon itself. When a function is defined as action, it gains the ability to utilize and differentiate between self and invoker. Non-action functions, on the other hand, consider self and invoker to be the same pointer.

The use of these pointers and action functions will be covered in more detail in the Weapons, overlays and PSprite chapter.

As for creating functions for regular actors (the ones that are not based on either Weapon or CustomInventory), for all intents and purposes it doesn’t matter if those functions are action or not. If a monster’s function is defined as an action function, it will still recognize invoker, but it’ll be the same thing as self (which is not at all how it works for weapons). Other than that, defining an actor function as an action function doesn’t change what it can do. However, action functions can’t be virtual or static, since the action, virtual and static keywords are incompatible, so normally defining actor functions as action isn’t useful.

In short, keep the following points in mind:

Static functions

Static functions are functions that can be called from anywhere, by any class. They’re defined by using a static keyword:

static void MyFunction()
{
    [...] //function contents
}

Static functions are not bound to the class they’re defined in, because they can be called anywhere. However, static functions still have to be defined within some class, and to call them from another class, you’ll have to use the original class’s name as a prefix:

class StaticFunctionContainer : Actor
{
    static void MyStaticFunction()
    {
        [...] //function contents
    }
}

class SomeOtherClass : Actor
{
    States
    {
    Spawn:
        TNT1 A 0 NoDelay
        {
            StaticFunctionContainer.MyStaticFunction();
        }
    [...] //the rest of the actor's code
    }
}

Static functions are designed specifically to be available to all classes. However, that also means that static functions don’t have the concept of self: the class where they’re defined in (FunctionContainer in the example above) can’t be self because it serves simply as a container for the function, it isn’t actually placed in the game when the function is called; whereas the class that ends up actually calling the function (SomeOtherClass in the example above) is ignored, since the defined function can’t magically guess which class will be calling it.

Static functions are usually used to get some data. For example, here’s a simple sign function that returns -1 or 1 based on whether the number given to it is positive or negative:

class MathContainer : Actor
{
    static int Sign (double i) 
    {
        if (i >= 0)
        {
            return 1;
        }
        return -1;
    }
}

To call it you’d call MathContainer.Sign(<number>) where would be the required number.

Note that in this case the container class doesn’t even have to inherit from Actor, it could be based on Object, but inheriting from Actor gives it access to actor functions, which may be useful in some cases.

Many existing ZScript functions are static. For example, the InStateSequence function that allows to check which state sequence the actor is currently in. It can be used to do that outside of states, e.g.:

override void Tick()
{
    super.Tick();
    if (InStateSequence(curstate,FindState("Spawn")))
    {
        Spawn("RocketTrail",pos);
    }
}

This override will make the actor spawn a rocket trail behind itself but only while it’s in the Spawn sequence. Note, if you have a pointer to an actor, you can check which state that actor is currently in as well:

class FearfulCacodemon : Cacodemon
{
    override void Tick() 
    {
        super.Tick();
        if (target && InStateSequence(target.curstate, target.FindState("Missile")))
        {
            bFRIGHTENED = true;
        }
        else
        {
            bFRIGHTENED = false;
        }
    }
}

This version of a Cacodemon will check if its target is in a “Missile” state sequence (this is the sequence used by player pawns when the player is firing a weapon), and if so, it’ll set its FRIGHTENED flag to true, so the monster is feeling.

Note that we’re calling InStateSequence, not target.InStateSequence: this is precisely because the function is static, and the actual caller of it doesn’t matter. What does matter is that we use the correct pointers to the state (target.curstate checks the target’s current state) and state label (target.FindState() looks for a specific state label in the target), but who calls the function itself is irrelevant.

Note: the example above can be shortened as follows:

class FearfulCacodemon : Cacodemon
{
    override void Tick() 
    {
        super.Tick();
        bFRIGHTENED = (target && InStateSequence(target.curstate,target.FindState("Missile")));
    }
}

This is possible because (target && InStateSequence(target.curstate,target.FindState("Missile"))) is a condition, and like all conditions, it can be either true or false. That’s why we can use its result to set the value of a flag to true or false, respectively.

Virtual functions

Virtual functions are a special type of method that can be overridden in a child class, similarly to how child classes can override the parent’s flags, properties and states.

Virtual functions is a pretty big topic, so they’re covered in more detail in the next chapter.


🟢 «< BACK TO START

🔵 « Previous: Pointers and casting 🔵 » Next: Virtual Functions