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.
In this tutorial we will take the Revenant's Decorate implementation and update him to ZScript , And while at it we will make him deadlier to showcase some of ZScripts features. Pardon me butchering some of the terms, This tutorial is aimed at letting people do things in ZScript based purely off their ACS\Decorate knowledge. This tutorial is for people who know Decorate\ACS but find it confusing to start coding in ZScript
This tutorial assumes you have knowledge about the following:
Without further delay let's begin:
1) Create your wad\pk3. Please note that while this tutorial won't use directories, you are hugely encouraged to use them in a real mod.
2) Add a new entry and call it ZScript. View as text and make sure the text language is set to Zdoom ZScript. Save if you want to.
Spoiler: 3)Converting the revenant to ZScript
Copy Paste the Revenant's actor from the wiki to your file.
Replace the word ACTOR with Class in the revenant actor declaration. Rename the Revenant to anything you like to avoid redundancy errors\warnings.
Make sure the revenant inherits from Actor
Encircle\Enclose all the properties and flags with the code block Default { // Properties and flags go here! } ; Everything before states should reside within this new block.
Add ; to the end of every single flag and proprty. Your file should look like this by now :Click Here!
Spoiler: 4)Update the states
Please be careful in this step; You might forget a line and get bombarded with errors!
Add () to all actions, For example A_FaceTarget beecomes A_FaceTarget(). Make sure you updated all of them.
Add ; to the end of states, This includes states without any action happening in them as well as goto, Loop and Stop. This excludes the lines where you declared a state alias\name\whatever the hell you call it, such as See: . Your file should look like this now : Click Here
The file can be tested now! Run and try summoning him!
Spoiler: I don't know how to summon him
` to bring up console, Type Summon Nrevenant and hit enter (Replace NRevenant with whatever you named him) You can also bind it to a key, I recommend 2 binds :
Bind Y "Kill monsters"
Bind U "Summon Nrevenant"
Spoiler: 5) Smart Chase
Now let's do some fun stuff, Let's make the revenant wander around instead of chasing a player hiding behind a wall! We will use a function for this step (just to showcase using one, we don't need to use one!). You can add your functions anywhere in the class, I like adding them before Defaults or at the very end after states so they don't get in the way when I just want to add properties\flags\states to the actor.
Add void A_SmartChase() { } before the default block. A_ is optional, It helps to know this is an action if you need to update the revenant in the future.
Add this code as the function's body (This is how it should look now) :
void A_SmartChase() // void => Returns nothing when executed, A_SmartChase() is how we call\execute this function (Action)
{
if (CheckSight(target)== true) // target means the revenant's prey is in sight
{A_Chase(); // if in sight then chase him normally
A_PrintBold("I see you!"); // Just for Testing, Remove or Commentate to disable
}
else
{
A_Wander(); // Wander is less predictable than A_chase, this stops revenant from rushing to people hiding behind walls.
A_PrintBold("Where are you!"); // Just For Testing
}
}
Replace A_Chase in the revenant's See state with our new smarter chase.
Test it, You can noclip if you want to see how the revenant walks while he can't see you. When done either commentate (Recommended) or remove the debug print messages.
This is how it should look by now : Click Here!
I forgot to provide updated versions of the file for the following steps, I am sorry! But you'll have to compare with the complete finished file if something doesn't work.
Spoiler: 6) Dangerous Melee
Now let's make the revenant's punch more dangerous, Here we will show executing multiple actions in a single state.
[*] We will be working on the Melee State. Let's merge the FaceTarget and SkelWhoosh actions in a single state :
To do this we encircle a states action with brackets (Again, a block of code). All the actions in the code block will execute linearly. You can modify variables, do checks etc.
Add A_Recoil(-10); between A_FaceTarget(); and A_SkelWhoosh();
Add Recoil in the same way to the H and I states.
This is how the Melee state look now, Test it ingame if you like
Melee:
SKEL G 6 { // Everything between the 2 brackets should execute!
A_FaceTarget();
A_Recoil(-16);
A_SkelWhoosh();
} // G6 Done, Remember they execute in order!
SKEL H 6 {
A_FaceTarget();
A_Recoil(-16);
}
SKEL I 6 {
A_Recoil(-16);
A_SkelFist();
}
Goto See;
Spoiler: 7) Projectile Check
In the same way, Let's make the revenant refuse to start a projectile attack if an enemy is in the way!
The following code should do it, Notice how the frame that tests it lasts 0; This allows a smart player to still use the 10 seconds before firing to run behind a monster.
You can test it using a mastermind in the way.
Missile:
SKEL J 0 Bright {
if(CheckSight(target)) // Attack if enemy hides behind wall
if(CheckLof()==false) // Is something in the way
{
SetStateLabel("See"); // Go t see if enemy in the way
A_PrintBold("OUT OF THE WAY"); // Debug. Remove or Commentate when done testing.
}
}
SKEL J 10 A_FaceTarget();
SKEL K 10 A_SkelMissile();
SKEL K 10 A_FaceTarget();
Goto See;
A_SkelMissile() hides a lot of info from us, Let's use it to make a new smarter version, and use this to show a new neat way to tidy up your project while at it!
Look in GZDoom.pk3\ZSCript\Doom\Revenant.txt for the action function's code. Copy it.
Extend Class NRevenant // Tells GZDoom we want this function in our revenant
{ // You can add more than just one new function to a class this way, Check the wiki!
void A_NSkelMissile() // New function
{
A_PrintBold("New Function works!"); // Debug, Commentate when done testing
if (target == null) return; // Don't shoot if target is gone
A_FaceTarget(); // Face Target before shooting
AddZ(16); // Let's keep it
Actor missile = SpawnMissile(target, "RevenantTracer"); // Shoots a revenantTracer toward target
AddZ(-16); // Let's Keep it
if (missile != null) // Shot Successfully
{
missile.SetOrigin(missile.Vec3Offset(missile.Vel.X, missile.Vel.Y, 0.), false); // Keep it
missile.tracer = target; // Who the missile chases, aka the revenant's target
if(CheckLof()== false) // If someone gets in the way in the last second
missile.AddZ(50,true); // Make the missile go up, Doesn't care if it hits the ceiling
}
}
}
Replace A_SkelMissile() in the revenant's missile state with our new one.
Spoiler: Let's try variables, Virtual Functions and some more conditions
Above the default block within our revenant add int punchLimit;
We want to initialize that variable to be 1, We override the PostBeginPlay function to initialize it ; You can read about PostBeginPlay() on the wiki under virtual functions, Basically it executes after our revenant is created but before his first tic happens.
The following block should do the trick, add it anywhere in the revenant or in the extended Class block (Since we are using it like a constructor function for our revenant, It is probably wiser to place this one in the revenant himself, At the top with the variables.
int punchLimit; // We want our Revenant to try to not focus on punching like an idiot
override void PostBeginPlay()
{
//A_PrintBold("Hello I am alive!");
punchLimit = 1; // Max punches allowed before shooting
Super.PostBeginPlay(); // call the super function for virtual functions so we don't break shit if GZdoom update.
}
Modify Melee and Missile states to use the new variable
Melee:
SKEL G 6 {
if(punchLimit > 0) // Has punches remaining
{
punchLimit--; // Lower punch counter
}
else
{
SetStateLabel("Missile"); // No punches => Go shoot
}
A_FaceTarget();
A_Recoil(-16);
A_SkelWhoosh();
}
SKEL H 6 {
A_FaceTarget();
A_Recoil(-16);
}
SKEL I 6 {
A_Recoil(-16);
A_SkelFist();
}
Goto See;
Missile:
SKEL J 0 Bright { punchLimit = 1; // Allow punches
if(CheckSight(target)) // Attack if enemy hides behind wall
if(CheckLof()==false) // Is something in the way
{
SetStateLabel("See"); // Go t see if enemy in the way
//A_PrintBold("OUT OF THE WAY"); // Debug, Remove or commentate when done
}
}
When you test it you'll notice the revenant makes his melee sound if he goes to missile from the melee state, we can fix this if we put the punch checks in a stand alone state's actions block, I kept it because it lets the revenant dash a bit and the sound is always hilarious to hear.
Don't give up when something doesn't work immediately, This will take some time to get used to.
Use one structure in your whole project for the classes. Don't make it horrible to debug! A generally safe structure for the classes would be something like this
Class ClassName : Actor // Or Parent
{
/*
All Variables declared here
*/
override void PostBeginPlay()
{
/*
Initialize the class
*/
Super.PostBeginPlay();
}
Default
{
/*
Flags First
*/
/*
Properties Afterward!
*/
}
States
{
/*
States Here. Rememeber to use TAB to tidy it up!
Example:
See:
AAAA X 4 {
/*
Variable Modifications;
Actions();
*/
}
Stop;
*/
}
}
Extend Class ClassName // To Add Functions
{
void FunctionName() // New function
{
/*
Variables and Initializations
*/
/*
Actions and Function calls
*/
}
}
You can find the body for the functions shown in here inside GZdoom.pk3 similar to how we extracted A_SkelMissile and used it to create a new one. This can help if you want to see how a specific function work but the wiki isn't updated yet.
Update your slade Text Editor config in Edit > Preferences > Text Editor : I use these settings , You can also collapse Text Editor on the left side to show color options; Dark mode recommended!
Read about Virtual Functions. And stay the hell away from Tick() unless you know what you are doing (You can easily shred performance if you ruin it), Don't forget to call the Super version of those functions after your modifications!
All Feed back welcome, Especially if a certain step is completely ambiguous or a better way to format the tutorial.
This seems pretty useful for someone who wants to learn ZScript.
Flags don't need a ';', by the way. They work fine without it. (This only applies to flags. Properties and variables still need it.)
IIRC the only reason you can even do that is because Graf added code to let you do that for consistency. (Since everything else ends with ';'.)
Good to know, I got to the habit of spamming semicolons just in case tbh . Can't believe I forgot to add a section to mention flags can be modified by typing bflagname as well. Will try to update the guide with it later.
They don't really serve the exact same purpose, This one doesn't go into much details before getting started ; very low level tutorial. The first link is definitely a must read after this tutorial though!