[ZScript] Breaking the ice for non-programmer DECORATE users

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

Moderators: GZDoom Developers, Raze 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.
User avatar
Matt
Posts: 9696
Joined: Sun Jan 04, 2004 5:37 pm
Preferred Pronouns: They/Them
Operating System Version (Optional): Debian Bullseye
Location: Gotham City SAR, Wyld-Lands of the Lotus People, Dominionist PetroConfederacy of Saudi Canadia

[ZScript] Breaking the ice for non-programmer DECORATE users

Post by Matt »

So I was reading this thread here and I was thinking if I could write a step-by-step example of how to do some basic stuff in ZScript that is straightforward enough for non-programmers with some DECORATE experience to understand immediately but still clearly capable of showing how deep you can get into the workings of how actors are spawned and moved. Based largely on my own experience working with projectiles and explosions these past 2 months.

Two points to keep in mind about this tutorial:
1. It looks stupidly long because I'm documenting every step and re-posting the entire modified code each time. It is intentionally repetitive.
2. I do not intend anyone to use this code as a precedent. There are numerous decisions here that are actually suboptimal and could be changed to make it more efficient or maintainable, but are left this way since it follows the flow of the tutorial itself and I really don't want to get into coding best practices or whatever because that would be the blind person with the poor spatial memory and Dunning-Kruger leading the blind.

Premise: are you tired of having to walk all the way over to a zombieman's corpse to pick up his clip every time you kill one? With this mod you can double your efficiency having the clips spawn only half as far away!

First, create a plaintext file called "zscript.txt" and copypaste the following:

Code: Select all

version "3.3" // you must enter this (or the appropriate number)
              // or you will be limited to features from 2.3.0

class LazyZom:Zombieman replaces Zombieman{} 
(That's "class", not "actor", because there are things that can get this treatment that aren't actors (and "actor" as a declaration is used for something else as we'll get to below). When converting existing projects, it's also advisable to do a string search immediately after for "damagefclass", etc. because that error will happen at least once.)

Now when you run it it should make no difference whatsoever (except breaking a few dynamic light definitions). Not very exciting! So let's start working on this for real...

The first real change we need is to get rid of the default dropitem so at the end of the day we should only have the one that drops at the closer distance. Unlike DECORATE, all flags and properties must be under a "default" section, and properties (not flags!) must end in a semicolon:

Code: Select all

version "3.3"

class LazyZom:Zombieman replaces Zombieman{
    default{
        dropitem "none";
    }
} 
Now we need to spawn the actual clip. Since we're really lazy and don't even want to wait for those extra 5 tics, we'll just call the event at the very start of the animation and use goto for the rest:

Code: Select all

version "3.3"

class LazyZom:Zombieman replaces Zombieman{
    default{
        dropitem "none";
    }
    states{
    death:
        ---- A 0 A_LazyDrop("Clip");
        goto super::death;
    xdeath:
        ---- A 0 A_LazyDrop("Clip");
        goto super::xdeath;
    }
} 
(There's a better way to do this without having to redefine both states but let's not get too far ahead. Read up on "virtual functions" in the wiki for that.)

Note the different syntax: All state lines must end in semicolons and all function calls must end with () even if they don't normally take any parameters.

Now obviously we haven't actually defined A_LazyDrop so the above won't run. Just to make sure it runs at all and we don't have any stray brackets, let's just put in an empty function:

Code: Select all

version "3.3"

class LazyZom:Zombieman replaces Zombieman{
    default{
        dropitem "none";
    }
    states{
    death:
        ---- A 0 A_LazyDrop("Clip");
        goto super::death;
    xdeath:
        ---- A 0 A_LazyDrop("Clip");
        goto super::xdeath;
    }
    void A_LazyDrop(class<Actor> todrop = "Clip"){}
} 
Now we've defined a function, A_LazyDrop, that is available to LazyZom and any and all descendants. It takes one parameter, which if left undefined defaults to "Clip".

At this point you should just get a normal zombieman that drops nothing. Now for the fun part - making it drop the thing!

Code: Select all

version "3.3"

class LazyZom:Zombieman replaces Zombieman{
    default{
        dropitem "none";
    }
    states{
    death:
        ---- A 0 A_LazyDrop("Clip");
        goto super::death;
    xdeath:
        ---- A 0 A_LazyDrop("Clip");
        goto super::xdeath;
    }
    void A_LazyDrop(class<actor> todrop = "Clip"){
        Spawn(todrop, self.pos, ALLOW_REPLACE);
    }
} 
This is quite primitive. Now when a zombieman dies, a clip - not marked as dropped, perfectly still, giving the full 10 when picked up, replaced by any actor that "replaces Clip" - will instantly appear at its feet. Not exactly an improvement.

So let's at least mark it as dropped and give it something that kinda looks like the drop movement:

Code: Select all

version "3.3"

class LazyZom:Zombieman replaces Zombieman{
    default{
        dropitem "none";
    }
    states{
    death:
        ---- A 0 A_LazyDrop("Clip");
        goto super::death;
    xdeath:
        ---- A 0 A_LazyDrop("Clip");
        goto super::xdeath;
    }
    void A_LazyDrop(class<actor> todrop = "Clip"){
        actor a = Spawn(todrop, self.pos, ALLOW_REPLACE);
        a.bdropped = true;
        a.vel.x = random(-1,1);
        a.vel.y = random(-1,1);
        a.vel.z = 8;
    }
} 
To break that down: instead of just calling Spawn, we define a variable "a" which points to the thing that we thereby spawn. Then we can tell ZDoom that this actor "a" has to be marked as +DROPPED (all the flag variable names start with "b" where the +- sign would otherwise be) and should have a random XY velocity and a Z velocity of 3.

Note how all this stuff - velocity, flags, etc. - needs to be defined explicitly, unlike A_SpawnItemEx. This might seem like more work, but once you're used to it it sure beats trying to remember all 29+ A_SpawnItemEx flags, the assumptions they're intended to override and what side effects each one has!
[2017-12-28 performance/optimization note: For more advanced or complex uses you might actually want to go back to A_SpawnItemEx, since every step you're putting through the ZScript virtual machine makes your code run that much slower. Sometimes I find it useful to use the approach in this tutorial to begin with and then change it to a Decorate command that relies on native functions that don't put as much load on the VM.]

Now getting the amount right is a bit trickier. ZScript won't recognize the "amount" property right off the bat since it's not found in all actors, just inventory and ammo, so you need to use the magic word "let" to define a variable that ZScript knows for sure is a kind of ammo and thus will definitely have that property to set:

Code: Select all

version "3.3"

class LazyZom:Zombieman replaces Zombieman{
    default{
        dropitem "none";
    }
    states{
    death:
        ---- A 0 A_LazyDrop("Clip");
        goto super::death;
    xdeath:
        ---- A 0 A_LazyDrop("Clip");
        goto super::xdeath;
    }
    void A_LazyDrop(class<actor> todrop = "Clip"){
        actor a = Spawn(todrop, self.pos, ALLOW_REPLACE);
        a.bdropped = true;
        a.vel.x = random(-1,1);
        a.vel.y = random(-1,1);
        a.vel.z = 8;
        let aa = Ammo(a);
        if(aa) aa.amount *= 0.5;
    }
} 
So now that's done and you've got something that more or less behaves just like a dropped zombieman clip. Just for fun, let's also make this starting velocity relative to the dying zom by adding its velocity to the mix!

Code: Select all

version "3.3"

class LazyZom:Zombieman replaces Zombieman{
    default{
        dropitem "none";
    }
    states{
    death:
        ---- A 0 A_LazyDrop("Clip");
        goto super::death;
    xdeath:
        ---- A 0 A_LazyDrop("Clip");
        goto super::xdeath;
    }
    void A_LazyDrop(class<actor> todrop = "Clip"){
        actor a = Spawn(todrop, self.pos, ALLOW_REPLACE);
        a.bdropped = true;
        a.vel.x = random(-1,1);
        a.vel.y = random(-1,1);
        a.vel.z = 8;
        let aa = Ammo(a);
        if(aa) aa.amount *= 0.5;
        a.vel += self.vel;
    }
} 
Since it's the zombieman calling this, "self" means the zombieman. It does not have to be explicitly written (except in a few situations we're not getting into now), but it's left in here to make it clear which actor's velocity we're talking about.

This is, of course, doing the exact opposite of what we were advertising: go punch a zombie while berserk and the clip will end up even further away than it otherwise would!

So let's make that velocity inheritance contingent... and finally do that halfway spawning thing!:

Code: Select all

version "3.3"

class LazyZom:Zombieman replaces Zombieman{
    default{
        dropitem "none";
    }
    states{
    death:
        ---- A 0 A_LazyDrop("Clip");
        goto super::death;
    xdeath:
        ---- A 0 A_LazyDrop("Clip");
        goto super::xdeath;
    }
    void A_LazyDrop(class<actor> todrop = "Clip"){
        actor a = Spawn(todrop, self.pos, ALLOW_REPLACE);
        a.bdropped = true;
        a.vel.x = random(-1,1);
        a.vel.y = random(-1,1);
        a.vel.z = 8;
        let aa = Ammo(a);
        if(aa) aa.amount *= 0.5;
        if(target && target is "PlayerPawn"){
            a.SetXYZ( (target.pos + a.pos) * 0.5 );
        }else a.vel += self.vel;
    }
} 
(As a side note, generally computers multiply faster than they divide, so never use /2 when *0.5 will do.)

Now if the zombie has a target (gotta check this first to make sure the computer isn't trying to work with a null pointer) and that target is a player, it will set the clip's position to halfway between the zom and the target player instead of adding the zom's velocity.

In other words, if you shoot a zombie at 20 paces, the zombie will drop dead where it is while a clip will pop up out of thin air only 10 paces away! Have fun!

...except there's a bug!

The way it's written now, sometimes the clip will hit a stair and get stuck in midair and unable to be picked up. It seems that calling SetXYZ right in the middle of setting everything up like that might have some side effects involving the engine trying to figure out which sector it's in.

To get around that, we'll just rewrite the conditional so that the initial spawning point is already the point that we want:

Code: Select all

version "3.3"

class LazyZom:Zombieman replaces Zombieman{
    default{
        dropitem "none";
    }
    states{
    death:
        ---- A 0 A_LazyDrop("Clip");
        goto super::death;
    xdeath:
        ---- A 0 A_LazyDrop("Clip");
        goto super::xdeath;
    }
    void A_LazyDrop(class<actor> todrop = "Clip"){
        actor a;
        if(target && target is "PlayerPawn"){
            a = Spawn(todrop, (target.pos + self.pos) * 0.5, ALLOW_REPLACE);
        }else{
            a = Spawn(todrop, self.pos, ALLOW_REPLACE);
            a.vel += self.vel;
        }
        a.bdropped = true;
        a.vel.x = random(-1,1);
        a.vel.y = random(-1,1);
        a.vel.z = 8;
        let aa = Ammo(a);
        if(aa) aa.amount *= 0.5;
    }
} 
No more stair-based weirdness! (I still have no real idea why it does this, though... and this will be something you'll learn to live with the more of this stuff you do until more of that coding stuff sinks in.)



Bonus: You can also give the poor zombieman a parting shot on the same principles:

Code: Select all

version "3.3"

class LazyZom:Zombieman replaces Zombieman{
    default{
        dropitem "none";
    }
    states{
    death:
        ---- A 0 A_LazyDrop("Clip");
        goto super::death;
    xdeath:
        ---- A 0 A_LazyDrop("Clip");
        goto super::xdeath;
    }
    void A_LazyDrop(class<actor> todrop = "Clip"){
        actor a;
        if(target && target is "PlayerPawn"){
            a = Spawn(todrop, (target.pos + self.pos) * 0.5, ALLOW_REPLACE);
            self.bsolid = false; //since this is happening before A_NoBlocking()
            actor b = Spawn("RevenantTracer", pos + (0,0,32), ALLOW_REPLACE);
            b.tracer = target;
            b.target = self;
            b.A_FaceTracer(0,0);
            b.A_PlaySound("skeleton/attack");
            b.vel.x = cos(angle) * cos(pitch) * 10;
            b.vel.y = sin(angle) * cos(pitch) * 10;
            b.vel.z = -sin(pitch) * 10;
        }else{
            a = Spawn(todrop, self.pos, ALLOW_REPLACE);
            a.vel += self.vel;
        }
        a.bdropped = true;
        a.vel.x += random(-1,1);
        a.vel.y += random(-1,1);
        a.vel.z += 8;
        let aa = Ammo(a);
        if(aa) aa.amount *= 0.5;
    }
} 
(The mnemonic I use when I need something to move forwards: X should be cosine of both angle and pitch, all horizontals are cosine of pitch, angles are irrelevant to Z, and don't forget that negative pitch means positive velocity.)

And, just for kicks, a rocket that inherits your momentum just as it's finished entering the playsim:

Code: Select all

version "3.3"

class AwkwardRocket:Rocket replaces Rocket{
    override void PostBeginPlay(){
        super.PostBeginPlay(); //does all the parent actor's initialization stuff
        if(target) vel += target.vel;
    }
} 
All done in a fraction of the amount of text it would take to... well, I don't even want to think about the DECORATE code I used to need for this.

And, of course, if you really want to feel the full sense of initiation:

Code: Select all

version "3.3"

class HelloRocket:Rocket replaces Rocket{
    override void PostBeginPlay(){
        super.PostBeginPlay();
        if(target){
            vel += target.vel;
            if(target is "PlayerPawn") target.A_Log("Hello world!",true);
        }
    }
} 
Last edited by Matt on Tue May 08, 2018 12:50 pm, edited 10 times in total.
User avatar
Matt
Posts: 9696
Joined: Sun Jan 04, 2004 5:37 pm
Preferred Pronouns: They/Them
Operating System Version (Optional): Debian Bullseye
Location: Gotham City SAR, Wyld-Lands of the Lotus People, Dominionist PetroConfederacy of Saudi Canadia

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Matt »

Postscriptum: So I asked about the SetXYZ thing and here's what Graf said:
Yes, you have to be careful. This function only alters the position but does not alter the blockmap links. This can cause some serious glitches.
If you want to change an actor's position in the world, use SetOrigin. SetXYZ is only for some special cases where the actor either isn't linked into the blockmap or for temporarily changing its position and restoring the old one later.
(emphasis mine)

So we can just go back to the last version with the SetXYZ and change it to SetOrigin:

Code: Select all

class LazyZom:Zombieman replaces Zombieman{
	default{
		dropitem "none";
	}
	states{
	death:
		"----" A 0 A_LazyDrop("Clip");
		goto super::death;
	xdeath:
		"----" A 0 A_LazyDrop("Clip");
		goto super::xdeath;
	}
	void A_LazyDrop(class<actor> todrop = "Clip"){
		actor a = Spawn(todrop, self.pos, ALLOW_REPLACE);
		a.bdropped = true;
		a.vel.x = random(-1,1);
		a.vel.y = random(-1,1);
		a.vel.z = 8;
		let aa = Ammo(a);
		if(aa) aa.amount *= 0.5;
		if(target && target is "PlayerPawn"){
			a.SetOrigin( (target.pos + a.pos) * 0.5, false);
		}else a.vel += self.vel;
	}
}
User avatar
Zergeant
Posts: 108
Joined: Tue Aug 31, 2010 7:19 am
Location: Sweden

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Zergeant »

Very nice and informative, covering both actor creation and defining custom functions. Quite useful indeed!
User avatar
Zergeant
Posts: 108
Joined: Tue Aug 31, 2010 7:19 am
Location: Sweden

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Zergeant »

Since the main post doesn't cover semantic for creating your own actor I'll just add a little quickly thrown-together example here:

Code: Select all

Class ZImp : Actor
{
	Default
    {
		Health 30;
		Radius 9;
		Height 8;
		Speed 12;
		Damage 4;
		Mass 100;
		Monster;
		RenderStyle "Add";
		SeeSound "cyber/sight";
		DeathSound "weapons/rocklx";
    }
    States
    {
    Spawn:
        TROO AB 10 A_Look();
        Loop;
	See:
		TROO AABBCCDD 2 A_Chase();
		Loop;
	Melee:
		TROO EFG 5 A_TroopAttack();
		Goto See;
	Pain:
		TROO H 2 A_Pain();
		Goto See;
    Death:
        MISL BCD 4 Bright;
        Stop;
    }
}
For actors that doesn't inherit from pre-existing actors like a Zombieman you'll need to inherit the base Actor.
This creates a translucent Imp with only melee attacks. Very similar in fashion to decorate so there should be no trouble at all. The key difference is that all the properties go into a "Default" block.

You also have your WAD and PK3 archives. For WADs you can just create a lump called "ZSCRIPT" and put everything in there, akin to a Decorate lump. For PK3 as the main post said you'll need to create a "ZSCRIPT.txt" and it will load the stuff automatically.
RaiFighter
Posts: 7
Joined: Sat Nov 18, 2017 6:09 pm

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by RaiFighter »

I could see this example by itself being useful. I'm still utterly new to this, so correct me if I'm wrong, but it seems to me like someone could change the item drop, have it spawn directly under the player so they insta-grab it, and more-or-less have part of an XP system there.
User avatar
Kinsie
Posts: 7402
Joined: Fri Oct 22, 2004 9:22 am
Graphics Processor: nVidia with Vulkan support
Location: MAP33

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Kinsie »

RaiFighter wrote:I could see this example by itself being useful. I'm still utterly new to this, so correct me if I'm wrong, but it seems to me like someone could change the item drop, have it spawn directly under the player so they insta-grab it, and more-or-less have part of an XP system there.
Players don't grab items unless they're moving so things'd be a bit weird if you killed a monster while standing still. Besides, [wiki]A_GiveToTarget[/wiki] exists for exactly this kind of thing.
RaiFighter
Posts: 7
Joined: Sat Nov 18, 2017 6:09 pm

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by RaiFighter »

Like I said, I'm still utterly new. Good to learn, though. :)
User avatar
Zen3001
Posts: 412
Joined: Fri Nov 25, 2016 7:17 am
Location: some northern german shithole

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Zen3001 »

I don't want to create a thread for this, I'm trying to learn zscript
I uselly learn stuff like this trough experimenting all I want are some ideas what I should be trying to create and a few resources to use
User avatar
Matt
Posts: 9696
Joined: Sun Jan 04, 2004 5:37 pm
Preferred Pronouns: They/Them
Operating System Version (Optional): Debian Bullseye
Location: Gotham City SAR, Wyld-Lands of the Lotus People, Dominionist PetroConfederacy of Saudi Canadia

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Matt »

Pretty much all ZScript that I know comes from having stuff I want to do and tried to do that I wasn't able to do (or at least wasn't able to do without lots of gross hacks with nasty side effects) with Decorate or ACS.

An example of that thought process. This particular example is actually something I've been wanting to do since 2004.


The rest of the ZScript that I know comes from trying to re-do existing Decorate/ACS work either to make it more efficient, or to set up a more modular framework so I can work from some common templates without copypasting everything all the time, or to just get rid of non-necessary ACS because I don't want to bother with maintaining a separate compiled blob. The best way to learn ZScript generally is to just convert part of an existing Decorate work and see if you can make any improvements doing so.


As far as resources go, I tend to stick with stock Doom stuff though I'm sure there are sprites on ream667 or elsewhere and there's always Sound Bible.
User avatar
Jekyll Grim Payne
Global Moderator
Posts: 1108
Joined: Mon Jul 21, 2008 4:08 am
Preferred Pronouns: He/Him
Graphics Processor: nVidia (Modern GZDoom)

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Jekyll Grim Payne »

There are still so many things I don't understand...

Code: Select all

void A_LazyDrop(class<Actor> todrop = "Clip"){}
What is class<Actor> and why is it written this way? What is todrop? I understand that you're defining a new function with its own parameters but I don't really understand how those parameters are defined, the syntax and structure of that definition, the—for the lack of a better word—rules of doing that.

Code: Select all

let aa = Ammo(a);
I understood what "a" was and where it came from. Can't say the same for "aa"... To be honest, the structure of this string is kinda confusing even though I understand the general idea.

Code: Select all

if(target && target is "PlayerPawn")
Why are there two targets in the statement?


For me at this point the hardest part is lack of information on how functions work, of course, but I guess the time should help with that.
Blue Shadow
Posts: 5032
Joined: Sun Nov 14, 2010 12:59 am

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Blue Shadow »

Jekyll Grim Payne wrote:

Code: Select all

void A_LazyDrop(class<Actor> todrop = "Clip"){}
What is class<Actor> and why is it written this way? What is todrop?
It's a data type, like string, int and bool. It takes classes, actor classes in this case. todrop is the name of the variable.

Code: Select all

let aa = Ammo(a);
I understood what "a" was and where it came from. Can't say the same for "aa"...
Someone else could explain this better, but, aa is a newly-declared variable which holds reference to the same object as a (the Clip actor), but as an Ammo instead of Actor. This way all the variables of the Ammo class, like Amount and MaxAmount and so on, can be accessed. This is what's known as casting.

You could swap let with Ammo in that case, and it'll be the same:

Code: Select all

Ammo aa = Ammo(a);

Code: Select all

if(target && target is "PlayerPawn")
Why are there two targets in the statement?
The first one is to check if the target actor of the calling actor exists (it's important you do that before trying to access it). The second one is to check if the class of said target actor is a PlayerPawn or is derived from it (in other words, if the target actor is a player).
User avatar
Jekyll Grim Payne
Global Moderator
Posts: 1108
Joined: Mon Jul 21, 2008 4:08 am
Preferred Pronouns: He/Him
Graphics Processor: nVidia (Modern GZDoom)

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Jekyll Grim Payne »

Is it possible to introduce a condition for a part of a state, not for a function? Like, if a custom CVAR = 1, I want to see these frames, and if that custom CVAR = 0, I want to see those frames?

Also, I'm not quite sure I understand what's the difference between void <custom function name> and action void <custom function name>, I've seen both used.
User avatar
Matt
Posts: 9696
Joined: Sun Jan 04, 2004 5:37 pm
Preferred Pronouns: They/Them
Operating System Version (Optional): Debian Bullseye
Location: Gotham City SAR, Wyld-Lands of the Lotus People, Dominionist PetroConfederacy of Saudi Canadia

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Matt »

[wiki]A_JumpIf[/wiki] is great for that sort of thing, for instance

Code: Select all

A_JumpIf((CVar.FindCVar("sv_mycvar").GetInt() < 2) && (health > 0), "otherstate")
should work.

here's how cvars are accessed. (If you try this code remember to define the cvar in question as a server cvar in [wiki]CVARINFO[/wiki] or you'll break multiplayer!)

Adding "action" in that context means that it could be called from the weapon's states or a CustomInventory's pickup and use states, and the "self" in that action script would be the owner/user.
User avatar
Zan
Posts: 338
Joined: Sat Oct 22, 2016 12:43 pm
Location: The depths of Hedon.

Re: [ZScript] Breaking the ice for non-programmer DECORATE u

Post by Zan »

Cool tutorial, great work!

Return to “Tutorials”