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

Handy guides on how to do things, written by users for users.
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] Breaking the ice for non-programmer DECORATE users

Postby Matt » Thu Mar 02, 2017 2:08 am

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 allExpand view
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 allExpand view
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 allExpand view
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 allExpand view
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 allExpand view
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 allExpand view
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.= random(-1,1);
        a.vel.= random(-1,1);
        a.vel.= 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 allExpand view
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.= random(-1,1);
        a.vel.= random(-1,1);
        a.vel.= 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 allExpand view
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.= random(-1,1);
        a.vel.= random(-1,1);
        a.vel.= 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 allExpand view
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.= random(-1,1);
        a.vel.= random(-1,1);
        a.vel.= 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 allExpand view
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.= random(-1,1);
        a.vel.= random(-1,1);
        a.vel.= 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 allExpand view
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.= cos(angle) * cos(pitch) * 10;
            b.vel.= sin(angle) * cos(pitch) * 10;
            b.vel.= -sin(pitch) * 10;
        }else{
            a = Spawn(todrop, self.pos, ALLOW_REPLACE);
            a.vel += self.vel;
        }
        a.bdropped = true;
        a.vel.+= random(-1,1);
        a.vel.+= random(-1,1);
        a.vel.+= 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 allExpand view
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 allExpand view
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 1:50 pm, edited 10 times in total.
User avatar
Matt
Putting the XD into *xdeath since 2007
 
Joined: 04 Jan 2004
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

Postby Matt » Fri Mar 03, 2017 1:08 am

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 allExpand view
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
Matt
Putting the XD into *xdeath since 2007
 
Joined: 04 Jan 2004
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

Postby Zergeant » Fri Mar 03, 2017 2:36 pm

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

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

Postby Zergeant » Sun Mar 26, 2017 10:13 am

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 allExpand view
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.
User avatar
Zergeant
 
Joined: 31 Aug 2010
Location: Sweden

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

Postby RaiFighter » Sun Nov 19, 2017 1:33 pm

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.
RaiFighter
 
Joined: 18 Nov 2017

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

Postby Kinsie » Mon Nov 20, 2017 7:18 pm

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, A_GiveToTarget exists for exactly this kind of thing.
User avatar
Kinsie
Ring of Death
 
Joined: 22 Oct 2004
Location: MAP33
Discord: Find Me...
Twitch ID: thekinsie

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

Postby RaiFighter » Mon Nov 20, 2017 8:55 pm

Like I said, I'm still utterly new. Good to learn, though. :)
RaiFighter
 
Joined: 18 Nov 2017

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

Postby Zen3001 » Wed Jan 03, 2018 12:29 pm

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
Zen3001
Hopefully I'll die soon
 
Joined: 25 Nov 2016
Location: A hellish northern german city
Discord: #0629

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

Postby Matt » Thu Jan 04, 2018 4:42 pm

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
Matt
Putting the XD into *xdeath since 2007
 
Joined: 04 Jan 2004
Location: Gotham City SAR, Wyld-Lands of the Lotus People, Dominionist PetroConfederacy of Saudi Canadia


Return to Tutorials

Who is online

Users browsing this forum: skornedemon and 2 guests