[ZScript] Making your first thinker

Handy guides on how to do things, written by users for users.

Moderator: GZDoom Developers

Forum rules
Please don't start threads here asking for help. This forum is not for requesting guides, only for posting them. If you need help, the Editing forum is for you.

[ZScript] Making your first thinker

Postby Tartlman » Wed Nov 06, 2019 1:34 pm

Thinkers are a feature that ZScript has that take the role of dummy actors being used to observe what's going on in game - however, thinkers do not truly exist in the game world - meaning that they take much less overhead, and are ideal for replacing such dummy actors. In this tutorial, we'll go over the basics on making thinkers. Before we start, i'm obligated to link the zdoom wiki page on thinkers: https://zdoom.org/wiki/Thinker. I'm also obligated to note that i haven't tested this code and am writing it by memory and hoping that it works

First of all, if we want to have a thinker, we need to make the thinker (duh). So let's do that:
Code: Select allExpand view
class myThinker : Thinker
{
}


It doesn't hold anything yet, but we can worry about that later. However, just having the thinker being defined isn't going to do much. We have to create the thinker, which can be done in anything. This could range from being inside an actor to being in an event handler, so let's assume that this is in some imaginary actor which will not be written. So note that directly copying from this tutorial will not work, since you need something that can actually run the functions.

And God said, "Let there be a thinker": And there was a thinker.
Code: Select allExpand view
class myThinker: Thinker
{
}

class actorshitorsomething : actor
{
    //blah blah blah actor stuff

    void createThinker()
    {
        myThinker t = new("myThinker");
    }
}


Now we have a thinker! by calling new("myThinker"); we create a thinker and then its pointer is assigned to t. We'll use t to manipulate the thinker's variables.

The thinker isn't doing much right now, and never will if we don't give it something that it can manipulate. So here, let's give it an actor pointer. First, we'll need to give the thinker an actor variable, and then we need to hand that variable an actor pointer. There's various ways of getting one of these, so i'm not going to go in to detail.

Code: Select allExpand view
class myThinker: Thinker
{
    actor a;
}

class actorshitorsomething : actor
{
    //blah blah blah actor stuff

    void createThinker()
    {
        myThinker t = new("myThinker");

        t.a = actorPointer;
    }
}


Now t.a points to an actor. You might be wondering where I got actorPointer from - well, I actually just pulled that out of my ass. There's several ways of getting a pointer to an actor so I'm not going to go into detail over that. So keep in mind that you'll need some value that is actorPointer. We can now use the thinker to manipulate the actor - note that while we can make the actor execute functions at will, the actor will still act the way it normally does. If you know how to do stuff with actors from a pointer, great. You can probably stop reading now. If not, let's make an example of what we can do!

We can hand our thinker more values this way - for example, if we defined an int in the thinker called "myInteger" and did something like "t.myInteger = 4;", then our thinker would get 4 assigned to myInteger.

Let's make a thinker that causes the actor to spawn health bonuses when it dies, depending on how much health it has! Let's start by creating and setting some values to help control the spawns.

Code: Select allExpand view
class myThinker: Thinker
{
    actor a;
    bool hasDied;

    override void PostBeginPlay()
    {
        super.PostBeginPlay();

        hasDied = false;
    }
}

class actorshitorsomething : actor
{
    //blah blah blah actor stuff

    void createThinker()
    {
        myThinker t = new("myThinker");

        t.a = actorPointer;
    }
}


We're going to use the variable hasDied to see if we've already spawned our bonuses after death. We don't want our actor to become an fps-murdering health fountain upon death.

"override void PostBeginPlay()" overrides the original PostBeginPlay() function. PostBeginPlay() is a function that executes exactly once after the actor is created - so it's great for initializing variables. Basically, what overriding this does is make it so that the thinker does what we define in there instead of what PostBeginPlay() did in the class it inherited from - in this case, PostBeginPlay() did nothing useful. "super.PostBeginPlay()" executes what the class it inherited from does in PostBeginPlay() - it's nothing in this case, but putting this in anyway is probably a good practice. Why didn't we do this in createThinker()? It's totally possible, but I personally like to give the thinker the bare minimum in the part where it's being created. It's up to you what you want to do though.

Now let's move on to actually checking if the actor is dead.

Code: Select allExpand view
class myThinker: Thinker
{
    actor a;
    bool hasDied;

    override void PostBeginPlay()
    {
        super.PostBeginPlay();

        hasDied = false;
    }

    override void Tick()
    {
        super.Tick();

        if (a != null)
        {
            if (a.health <= 0 && !hasDied)
            {
                hasDied = true;
            }
        }
}

class actorshitorsomething : actor
{
    //blah blah blah actor stuff

    myThinker t = new("myThinker");

    t.a = actorPointer;
}


The idea of overriding Tick() is the same as it is with PostBeginPlay(). Tick() is a function that executes every single tick - there's 35 ticks in a second. This function is the meat of what your thinker will do. Note that we check "a != null" before doing anything. This is so if the actor we're looking at is removed from existence somehow - say, by calling destroy() or removing it via the console - we don't end up trying to execute functions on a null pointer, which will lead to a crash. Now we have a check that sees whether the actor is dead and whether it's done its death function already - so let's do the spawning health bonuses part.

Code: Select allExpand view
class myThinker: Thinker
{
    actor a;
    bool hasDied;

    override void PostBeginPlay()
    {
        super.PostBeginPlay();

        hasDied = false;
    }

    override void Tick()
    {
        super.Tick();

        if (a != null)
        {
            if (a.health <= 0 && !hasDied)
            {
                hasDied = true;

                int spawnTimes = a.SpawnHealth() / 10;
                for (int i = 0; i < spawnTimes; i++)
                {
                    a.A_SpawnItemEx("HealthBonus", 0, 0, 0, random(-3, 3), random(-3, 3), random(0, 3));
                }
            }
        }
}

class actorshitorsomething : actor
{
    //blah blah blah actor stuff

    myThinker t = new("myThinker");

    t.a = actorPointer;
}


Now we can spawn the health bonuses. We're using a.SpawnHealth() to find the spawn health of our actor, and then dividing that by 10 to get how many health bonuses to spawn. Take note of the function inside the for loop - it's our good old buddy from the DECORATE days, A_SpawnItemEx! We can actually call any function that the actor has, as long as we do it as "a.A_DoSomething()". Since A_SpawnItemEx is a function that the class actor has, by calling it from our actor we can make it do the function as if you had made it in DECORATE.

And that should be the basics of thinkers!
User avatar
Tartlman
shitposting with zscript
 
Joined: 11 Oct 2018
Location: meme hell
Discord: Tartlman#2947

Return to Tutorials

Who is online

Users browsing this forum: No registered users and 1 guest