Jump to content

Photo
* * * * * 2 votes

[Scripting/2.50] ZScript tutorial 2 - intermediate


  • Please log in to reply
1 reply to this topic

#1 Saffith

Saffith

    IPv7 user

  • ZC Developers

Posted 22 December 2014 - 03:31 PM

Before beginning this tutorial, you should be comfortable with the material from the basic tutorial. The logic is more complex here, and there are more technical details to remember, so there are a lot more opportunities to make mistakes. This is also where you'll need to start thinking like a computer.

While loops
Conditional statements
Boolean algebra and comparisons
User-defined variables
For loops
Miscellaneous
User-defined functions and script arguments
 
 
 

While loops


If you want your script to run continuously, or if it should wait for something to happen, you need a loop. A loop is a piece of code that runs over and over again as long as a specified condition is met.

There are three types of loops in ZScript. The simplest of these is the while loop. The syntax is easy enough:

while(<condition>)
{
    <body>
}
 
The condition determines whether the loop will run. The condition can be a variable, a function call, or any other expression that can be interpreted as a Boolean value. The body, the code in between the braces, is the code that will be run if the condition is met. Note that there's no semicolon after the while statement. More on that shortly.

When the game reaches the loop, it first evaluates the condition. If it's false, it skips right past the loop. If it's true, it runs the code in the loop's body. When it reaches the closing brace of the body, it evaluates the condition again. If it's still true, it repeats; otherwise, it moves on.

// This will move Link to the right as long as A is held.
// This won't wait for A to be pressed; if the button isn't already
// being held when the loop is reached, it won't run.
while(Link->InputA)
{
    Link->X += 1;
    Waitframe(); // In most cases, a while loop's body should include Waitframe()
}

// You can use the Boolean literals true and false, as well. Using while(true)
// is very common, since many scripts are intended to run indefinitely.

// This will disable the B button by unpressing it every frame.
while(true) // The condition will always be true, so this loop will repeat forever
{
    Link->InputB = false;
    Waitframe();
}

// This won't do anything.
while(false) // The condition will never be true, so the body won't be run at all
{
    Link->Z = 32;
    Waitframe();
}

// Once a loop's body starts executing, it'll go all the way to the end,
// even if the condition becomes false partway through.
while(Link->InputA)
{
    Link->InputA = false; // The lines after this one will run
    Link->HP -= 1;
    Waitframe();
    // Remember that Link->InputA will be reset the next frame,
    // so this loop may run repeatedly
}

// You can nest one loop inside another.

// Refill Link's HP when A is held, repeating every time the button is pressed.
// It would actually make more sense to use a conditional statement than an inner
// loop in this case, but those won't be covered until the next section.
while(true)
{
    // If A isn't being held when the inner loop is reached, it will be skipped.
    // Since it's inside another loop, the game will check repeatedly.
    while(Link->InputA)
    {
        Link->HP += 1;
        Waitframe();
    }
    Waitframe(); // In this case, each loop needs its own Waitframe()
}
 
What happens if you use a semicolon after the while statement?

while(Link->InputA);
{
    Link->HP += 1;
    Waitframe();
}

// That's legal, but it's equivalent to this:
while(Link->InputA)
{
    // Do nothing
}
Link->HP += 1;
Waitframe();
 
Obviously, that's not what you would intend. The functional lines aren't in the loop's body at all. Worse than that, though, is that the body is empty. An empty while loop is a very bad thing.

It's vital to be understand that a script runs from one Waitframe() to the next in a single frame. The game engine only does one thing at a time; once it starts running a script, it won't move on to something else until the script ends or it reaches Waitframe() or Waitdraw(). One implication of this is that an infinite loop with no Waitframe() will hang the game. An infinite loop isn't just something like while(true). while(Link->InputA) is also infinite in that situation, because the button stays pressed for the entire frame. If the game doesn't advance, it will never update the input variables, so the condition will never become false. The good news is that if you do accidentally do this, you'll be able to figure it out pretty quickly when you run the script.

That's it for now. Thinking in terms of loops and per-frame updates takes some getting used to, but it's vital for complex scripts.

Here are some sample scripts that incorporate while loops.

// This script will draw a line connecting Link and the FFC.

ffc script ConnectingLine
{
    void run()
    {
        while(true)
        {
            // The line will flash because a random color is selected each frame
            Screen->Line(0, this->X + 8, this->Y + 8, Link->X + 8, Link->Y + 8, Rand(16), 1, 0, 0, 0, 128);
            Waitframe();
        }
    }
}
 
// This script will move the FFC toward Link, aiming again every two seconds.

import "std.zh"

ffc script ReaimRepeatedly
{
    void run()
    {
        while(true) // This will repeat indefinitely
        {
            // Aim toward Link
            this->Vx = (Link->X - this->X) / Distance(Link->X, Link->Y, this->X, this->Y);
            this->Vy = (Link->Y - this->Y) / Distance(Link->X, Link->Y, this->X, this->Y);
            
            // Wait two seconds, then the loop will repeat
            Waitframes(120);
        }
    }
}
 
// This script will drain Link's MP as long as B is held.
// (Again, nested loops aren't normally the way this would be done.)

ffc script MPDrainOnB
{
    void run()
    {
        while(true)
        {
            while(Link->InputB)
            {
                Link->MP -=1;
                Waitframe();
            }
            
            Waitframe();
            
            // Why are two Waitframes needed? If the inner loop didn't have a Waitframe call,
            // it would be a hanging infinite loop when B was pressed. If the outer loop didn't
            // have one, it would hang when B was not pressed.
        }
    }
}
 
And a couple of practice exercises.
  • Write a script that keeps the FFC's position opposite Link's. If Link is, say, one tile down from the top-left corner, the FFC will be one tile up from the bottom-right. This only takes a few lines; just figure out the mathematical relationship between the two's positions.
    Hint

    Spoiler
  • Draw a circle around Link that follows him as he moves.
    Spoiler

Conditional statements


As you can probably guess from the name, a conditional statement, or if statement, allows you to run code conditionally. That is, it only runs if a certain requirement is met. In its basic form, the syntax is like that of a while loop:

if(<condition>)
{
    <body>
}
 
Just like a while loop, the body will be run if the condition is true and skipped if it's false. The difference is that it will only run once.

There's no semicolon after the if statement.

// Move five pixels to the right if A is pressed
if(Link->InputA)
{
    this->X += 5;
}

// This refills Link's HP and MP if he has the White Sword.
if(Link->Item[I_SWORD2])
{
    Link->HP = Link->MaxHP;
    Link->MP = Link->MaxMP;
}

// You'll often want to put a conditional inside a loop.
// This refills half a heart each time A is pressed.
while(true)
{
    if(Link->PressA)
    {
        Link->HP += 8;
    }
    Waitframe();
}

// Again, you can use true or false for the condition. Unlike with loops, there's generally
// no reason to do this. You might do it temporarily for debugging, but it's pointless otherwise.

// Increase Link's HP by 16.
if(true)
{
    Link->HP += 16;
}
 
An if statement can be extended with an else clause. This lets you run code when the condition is false.

if(<condition>)
{
    <body 1>
}
else
{
    <body 2>
}
 
Body 1 will run if the condition is true, and body 2 will run if it's false. Only one of these will run; if the condition is initially true, body 2 won't run even if body 1 makes the condition false.

The else clause is optional, but the if is not. You can't have an else without an if. You also can't have more than one else for a single if.

// Move Link one tile to the left if A is pressed, and one tile to the right if it's not
if(Link->InputA)
{
    Link->X -= 16;
}
else
{
    Link->X += 16;
}
 
In addition to that, you can combine else and if into else if to handle multiple conditions.

if(<condition 1>)
{
    <body 1>
}
else if(<condition 2>)
{
    <body 2>
}
else if(<condition 3>)
{
    <body 3>
}
else
{
    <body 4>
}
 
If condition 1 is true, body 1 will run. If condition 1 is false and condition 2 is true, body 2 will run. If conditions 1 and 2 are false but 3 is true, body 3 will run. If all three conditions are false, body 4 will run. You can repeat this with as many conditions as you like.

These are mutually exclusive, and the earlier statements take priority. If all three conditions are true, only body 1 will be run. Think of else if as meaning "if every condition so far is false and this new condition is true..."

// Move Link one tile to the left if A is held, and one tile to the right is B is held but not A
if(Link->InputA)
{
    // If the player is holding both A and B, this is the block that will run
    Link->X -= 16;
}
else if(Link->InputB)
{
    Link->X += 16;
}
 
Some sample scripts...

// This script will heal Link while the B button is held.
ffc script BButtonHeal
{
    void run()
    {
        while(true)
        {
            if(Link->InputB)
            {
                Link->HP +=1;
            }
            Waitframe();
        }
    }
}
 
// This script will reverse the left and right buttons.
ffc script SwitchLeftAndRight
{
    void run()
    {
        while(true)
        {
            // If you don't see why it's done this way, try writing it differently
            // and work out what happens when both left and right are pressed
            if(Link->InputLeft)
            {
                // If both buttons are pressed, nothing changes
                if(Link->InputRight)
                {
                    // Do nothing
                }
                // If only left is pressed, unpress it and press right instead
                else
                {
                    Link->InputRight = true;
                    Link->InputLeft = false;
                }
            }
            // If only right is pressed, unpress it and press left instead
            else if(Link->InputRight)
            {
                Link->InputLeft = true;
                Link->InputRight = false;
            }
            
            Waitframe();
        }
    }
}
 
Practice exercises:
  • Write a script that makes the FFC move right if left is pressed and left if right is pressed.
    Spoiler
  • Write a global script that refills Link's MP when both L and R are held.
    Hint

    Spoiler

Boolean algebra and comparisons


Now that you know how to write loops and conditional statements, it's time to learn how to make more interesting conditions.

Boolean algebra is to bools what arithmetic is to numbers. Arithmetic combines numbers in various ways to produce a new number, and Boolean algebra combines bools to produce either true or false. ZScript has three logical operators:
  • && AND
  • || OR
  • ! NOT
AND: A && B is true if both A and B are true; otherwise, it's false.
OR: A || B is true if either A or B is true, or if both of them are; otherwise, it's false.
NOT: !A is true if A is false and false if A is true.

The order of operations is NOT, then AND, then OR. When using AND and OR together, however, it's always best to use parentheses to avoid confusion.

// Drain Link's HP while either A or B (or both) is held
while(Link->InputA || Link->InputB)
{
    Link->HP -= 1;
    Waitframe();
}

// Make Link jump if both A and B are pressed or if both L and R are pressed
if((Link->InputA && Link->InputB) || (Link->InputL && Link->InputR))
{
    Link->Jump = 5;
}

// Logical operations are mainly used in conditions, but they don't have to be.
// This is more useful with user-defined variables and functions, but still fairly rare.
Link->InputA = Link->InputL && Link->InputR; // A is pressed only if the player is pressing both L and R
Link->InputB = !Link->InputB; // If B is not pressed, press it; if it is pressed, unpress it

// Just like arithmetic, AND and OR can be combined with the assignment operator.
// The effects can be hard to understand at first, though.
Link->InputA &&= Link->InputB; // A can only be pressed if B is also pressed
Link->Item[100] ||= Link->Item[101]; // Give Link item 100 if he already has item 101
 
Arithmetic takes in numbers and spits out different numbers, and Boolean algebra combines bools into different bools. It's also possible to go from numbers to bools by using comparisons. These will evaluate to true or false based on the relationship between two numbers. This is done by using the relational operators:
  • == equal
  • != not equal
  • < less
  • > greater
  • <= less or equal
  • >= greater or equal
Equal: A == B is true if A and B are the same number
Not equal: A != B is true if A and B are not the same number
Less than: A < B is true if A is less than B
Greater than: A > B is true if A is greater than B
Less or equal: A <= B is true if A is less than or equal to B
Greater or equal: A >= B is true if A is greater than or equal to B

The == and != operators can also compare two bools.

Equal: A == B is true if A and B are both true or both false
Not equal: A != B is true if one of A and B is true and the other is false

<, >, <=, and >= are evaluated before == and !=. You'll probably never encounter a situation where that matters, but if you do, it's better to use parentheses to avoid confusion.

// If there are at least 8 enemy weapons on screen, remove them all
if(Screen->NumEWeapons() >= 8)
{
    Screen->ClearSprites(SL_EWPNS);
}

// If Ex1 is pressed and Link has at least 16 MP, use that much to heal one heart
if(Link->PressEx1 && Link->MP >= 16)
{
    Link->MP -= 16;
    Link->HP = Min(Link->MaxHP, Link->HP + 16);
}

// Make Link jump if he's on the ground and either A or B is pressed, but not both
if(Link->Z==0 && (Link->InputA != Link->InputB))
{
    Link->Jump = 8;
}

// As with logical operations, these don't just have to be used for conditions.
// There's no way to combine them with the assignment operator, though.
Link->Item[I_SWORD4] = Link->HP == Link->MaxHP; // Give Link the Master Sword if his HP is full, and take it away otherwise
Link->InputA = Link->HP < Link->MP / 2; // Press A if Link's HP is less than half his MP, and unpress it if not

if(Link->HP == Link->MP == 32) // ERROR: Explained below
 
The problem with that last example is that it's actually trying to do two things at once. Each comparison is a separate operation; the condition is equivalent to (Link->HP == Link->MP) == 32. After the first comparison is evaluated, you'll be left with true == 32, which is obviously wrong. The correct way to write the condition would be Link->HP == 32 && Link->MP == 32.

By combining logical and relational operators, you can create any condition you can imagine.

// This condition will be true if A or B is held, or if Link's X position is not between 32 and 96
if(Link->InputA || Link->InputB || !(Link->X >=32 && Link->X <= 96))

// This condition will be true if a random number is less than 7 and neither L nor R is pressed.
if(Rand(10) < 7 && !(Link->InputL || Link->InputR))
 
There always more than one way to write a condition. Sometimes there's one way that's clearly the most logical, and sometimes there's not. Just use whatever makes the most sense to you.

// These two are equivalent.
if(Link->HP >= 32) // If Link's HP is 32 or greater...
if(!(Link->HP < 32)) // If Link's HP is not less than 32...

// These two are equivalent.
if(!Link->InputA && !Link->InputB) // If A is not pressed and B is not pressed...
if(!(Link->InputA || Link->InputB) // If neither A nor B is pressed...

// These two are equivalent.
if(Link->InputA) // If A is pressed...
if(!(!Link->InputA)) // If A is not not pressed...
 
The sample scripts and practice exercises start to become more practical at this point.

// This script warps Link to another screen when the player presses A.

const int AWARP_DEST_DMAP = 1;
const int AWARP_DEST_SCREEN = 0x24;

ffc script WarpOnA
{
    void run()
    {
        // While A isn't pressed, just wait
        while(!Link->InputA)
        {
            Waitframe();
        }
        Link->Warp(AWARP_DEST_DMAP, AWARP_DEST_SCREEN);
    }
}
 
// This script makes the FFC act as a switch that opens the north door for ten seconds
// when Link steps on it.

import "std.zh"

ffc script DoorSwitch
{
    void run()
    {
        while(true)
        {
            // Check if Link has stepped on the switch.
            // The reason for using Abs() like this is to give a few pixels' leeway;
            // if the condition were Link->X == this->X && Link->Y == this->Y,
            // Link would have to be perfectly aligned with the FFC to trigger it.
            if(Abs(Link->X - this->X) < 4 && Abs(Link->Y - this->Y) < 4)
            {
                // Change the FFC's combo and open the door, then wait ten seconds
                // and do the opposite
                this->Data += 1;
                Screen->Door[DIR_UP] = D_OPENSHUTTER;
                Game->PlaySound(SFX_SHUTTER);
                
                Waitframes(600);
                
                this->Data -= 1;
                Screen->Door[DIR_UP] = D_1WAYSHUTTER;
                Game->PlaySound(SFX_SHUTTER);
            }
            
            Waitframe();
        }
    }
}
 
// This script will allow Link to spend MP to heal by holding L and R
// if he has the amulet.

import "std.zh"

global script HealingAmulet
{
    void run()
    {
        while(true)
        {
            // Sometimes, it's easier to split long conditions in two
            if(Link->Item[I_AMULET1] && Link->InputL && Link->InputR)
            {
                if(Link->HP < Link->MaxHP && Link->MP > 0)
                {
                    Link->MP -= 1;
                    Link->HP += 1;
                }
            }
            Waitframe();
        }
    }
}
 
Practice exercises:
  • Write a script to make Link jump if Ex1 is pressed while he's on the ground.
    Spoiler
  • Rewrite the SwitchLeftAndRight script from the previous section using logical operators instead of nested ifs.
    Spoiler
  • Write a script that plays a sound and opens a door if Link has two certain items or shows a message if he doesn't.
    Spoiler

User-defined variables


For more advanced scripts, it's frequently necessary to define your own variables. These allow you to store arbitrary data to do with as you please. User-defined variables differ from the built-in ones in a few major ways:
  • They must be declared before being used
  • They're not associated with any pointers
  • They don't represent anything in the game; they just store abstract data
Declaring a variable defines its name and the type of data it stores. Declaring a variable is essentially creating it; before it's declared, a variable doesn't exist. Therefore, you have to declare it before you can use it. To declare a variable, just write its type (int, float, or bool) and its name. The rules for valid variable names are the same as for scripts and constants.

When you declare a variable, you can also initialize it; that is, you can set its initial value. To do this, you simply assign it a value at the same time you declare it. If you don't, its value will default to 0 or false.

int var1; // Declares a variable named var1
float var2 = 10; // Declares and initializes a variable named var2
bool var3 = false; // Same thing, this time a Boolean variable

bool var2; // ERROR: A variable named var2 already exists
var1 = var4; // ERROR: var4 has not been declared
Link->X = int var5; // ERROR: You can't declare a variable and read its value at the same time
 
Don't declare a variable in the body of a loop or a conditional statement. This isn't necessarily a bad idea if you know what you're doing, but it creates the potential for scope-related errors, which can be very confusing to deal with. Scope will be discussed in the advanced tutorial.
int goodVar = 5;

if(Link->HP>=16)
{
    bool badVar = true;
    goodVar = 10;
}

Trace(goodVar); // OK: it's either 5 or 10
TraceB(badVar); // ERROR: badVar doesn't exist in this context
 
Unlike pointers' variables, user-defined variables are simply abstract data. They don't represent anything in the game, so nothing happens when you change them. Even so, they have countless uses.

// Sometimes, variables are unnecessary, but convenient.

// You can store the result of a calculation so you don't have to repeat it,
// or just to make the code easier to read and write.
float distance = Distance(Link->X, Link->Y, this->X, this->Y);
if(distance < 48)
{
    // ...
}
else if (distance < 96)
{
    // ...
}

// If you've got a very complex condition, a variable may be helpful in splitting it up.
if((Link->HP == Link->MaxHP && !(Link->InputA || Link->InputB)) ||
   (Link->HP <= 16 && Link->Item[200] && Screen->NumNPCs() > 10))

// You can split that in two like so:
bool doStuff = false;
if(Link->HP == Link->MaxHP && !(Link->InputA || Link->InputB))
{
    doStuff = true;
}
if(Link->HP <= 16 && Link->Item[200] && Screen->NumNPCs() > 10)
{
    doStuff = true;
}

if(doStuff)
{
    // Do whatever it is
}

// For some purposes, variables are necessary.

// If you want to remember a value over a period of time, you'll need to store it in a variable.
// This saves Link's position and restores it ten seconds later.
int oldLinkX = Link->X;
int oldLinkY = Link->Y;
Waitframes(600);
Link->X = oldLinkX;
Link->Y = oldLinkY;

// If you want to swap two variables, you'll need a third to store the one of the values temporarily.
int temp = Link->X;
Link->X = this->X;
this->X = temp;

// Variables can be used as counters.
// This disables A and B for 300 frames (5 seconds).
int frameCounter = 0;
while(frameCounter < 300)
{
    Link->InputA = false;
    Link->InputB = false;
    frameCounter += 1;
    Waitframe();
}

// Another use is to compensate for integer truncation.
// Link->X is strictly an integer; if you want to move him 1/4 pixel at a time,
// you can use a variable to store his "real" position.
float realLinkX = Link->X;
while(true)
{
    realLinkX += 0.25;
    Link->X = realLinkX;
    Waitframe();
}
 
There are two different types of user-defined variables. If you declare a variable in run(), it's called a local variable. Local variables only exist within run(), so other scripts can't use them. If the same script is used on more than one FFC or item, each one will have its own copy of every local variable, so they won't interact.

You can also declare a variable outside of the script. That makes it a global variable. Global variables are available to all scripts in a quest. There's only copy one of each, so changing it in one place will change it everywhere. Be sure to give global variables unique names, and be especially careful if your own scripts will be used alongside other people's. Multiple global variables can't have the same name, so this is an easy way for different scripts to conflict.

int globalVar = 100; // Global variable

ffc script Script1
{
    void run()
    {
        bool localVar = false; // Local variable
        globalVar = 50;
    }
}

ffc script Script2
{
    void run()
    {
        // This is separate from Script1's localVar and
        // separate from localVar in any other ffc running Script2.
        float localVar = 5;
        
        // globalVar is the same everywhere. If Script1 hasn't run yet,
        // it's still 100. If Script1 has run, it's 50.
        Trace(globalVar);
    }
}
 
If you initialize a global variable, it will only be initialized when the quest is first started. If you want it to be done every time Link dies or saves and continues, you need to set it at the beginning of the active global script.

In addition to local and global variables, there are also screen variables. These are actually built-in variables, but they're used similarly to user-defined ones. Each screen has its own copy of Screen->D[], an array of eight numbers which have no effect in the game. You can use them to store information or pass data between scripts without using global variables. Related to these are this->Misc[] (FFC only) and Link->Misc[], which can be used in much the same way (other pointer types also have Misc[], but that will come later). this->Misc[] is especially useful in large scripts when you'd otherwise have to pass lots of data to every function or when multiple copies of the same script need to communicate.

ZC remembers the values of global variables and every screen's Screen->D[] when the game is saved, so they can be used to control how scripts run later on in the quest, or just to store data indefinitely. Screen->D[] is often used so that a script knows if it's run on the current screen before.

There is an important caveat regarding the saving of global variables, however. Consider what happens if you save a quest, open it in ZQuest and swap all the items around, then load the saved game again. Now Link has all the wrong items, because the game only remembers the ID numbers of the items he had, not what ID goes to what item. Something similar can happen with global variables. When you compile scripts, each global variable is assigned to one of 256 "slots," and the game doesn't know which variable is in which slot. When you change your scripts and recompile, it may assign them differently, making the saved data incorrect. Therefore, when the global variables in your quest change, any existing saves should be considered invalid.

The subject of scope is too advanced to discuss in depth right now, but a couple of points have to be mentioned.

It's possible to have global and local variables with the same name. This can easily cause mistakes if you're not aware of it. One way to make sure you don't do it accidentally is to use different naming conventions for each. For instance, you might start every global variable's name with g_, or you could capitalize them differently.

In some cases, you can even have multiple local variables using the same name. This can be very confusing, as you may not be using the variable you think you're using . An easy way to avoid it is to declare all your local variables at the beginning of run(), before you do anything else.

One last thing to keep in mind. A quest can't have more than 255 global variables, so you should try not to use more of them than necessary. The limit on local variables is fuzzy; it can be up to 255, but it's usually fewer. If you do hit the limit, you likely need to reconsider how you're using them.

Finally, some sample scripts.

//This will make the FFC move across the screen to the right in a sine wave pattern.

const int SWR_AMPLITUDE = 32;

ffc script SineWaveRight
{
    void run()
    {
        int centerY = this->Y; // The center of the sine wave
        int counter = 0; // Used with the sine function to find the offset from center
        
        this->Vx = 1;
        while(true)
        {
            this->Y = centerY + SWR_AMPLITUDE * Sin(counter);
            Waitframe();
            
            // The counter increases by 1 each frame and resets to 0 when it hits 360
            counter = (counter + 1) % 360;
        }
    }
}
 
// These two scripts together create an item that makes Link float in the air.

bool floating = false; // This allows the item and global scripts to communicate

item script FloatItem
{
    void run()
    {
        floating = !floating; // Switches between true and false
    }
}

global script FloatGlobal
{
    void run()
    {
        // Make sure the variable gets reset after continuing the game
        floating = false;
        
        while(true)
        {
            if(floating)
            {
                if(Link->Z < 32)
                {
                    // Make Link rise up 1 pixel per frame
                    Link->Jump = 1;
                }
                else
                {
                    // Prevent Link from falling
                    Link->Jump = 0;
                }
            }
            Waitframe();
        }
    }
}
 
And practice exercises.
  • Write two item scripts: one that remembers the current screen and one that warps back to that screen.
    Hint

    Spoiler
  • Write an FFC script that refills Link's HP if he steps on the FFC, but only ever runs once on a given screen.
    Spoiler
  • Rewrite the SwitchLeftAndRight script again, but this time, do it with no conditional statements at all.
    Hint

    Spoiler

For loops


The second type of loop in ZScript is the for loop. The syntax is more complex than that of the while loop.

for(<initialization>; <condition>; <increment>)
{
    <body>
}
 
When the game encounters a for loop, the first thing it does is run the initialization statement. This is normally used to prepare a counter variable, but any statement is legal. Next, the game checks the condition. This is just the same as in a while loop. Any bool or Boolean expression is acceptable. If it's false, the loop is skipped. If it's true, the body is run. At the end of the body, the increment statement is run. This usually increases or decreases the counter variable, pushing it a little closer to making the condition false. After the increment, the condition is checked again. If it's still true, the body runs again. If not, the loop ends. The initialization and increment statements are optional, but the semicolons between them are not.

for and while are normally used differently, but each can do anything the other can do. To create an equivalent while loop, you would rearrange the for loop's elements like this:

<initialization>
while(<condition>)
{
    <body>
    <increment>
}
 
Likewise, a for loop with no initialization or increment is the same as a while loop.

If they can do the same things, why have both types of loops at all? Simply because each is better suited to different purposes, and using the more appropriate one makes code easier to read and write (once you get used to the syntax of each, of course). for loops are better for working with sets of objects, such as arrays or the list of enemies on the screen. They're generally better if you want the loop to run a certain number of times, and they're handy when you just need a counter variable that changes from one frame to the next. while loops are preferable when you have no use for a counter or timer. If you're just waiting for some condition to be met, or you want a simple infinite loop, a while loop makes more sense.

// When using a counter, it's common (but not necessary) to start at 0 and
// repeat while the counter is less than the total number of times through the loop.

// Heal 2 HP per frame for 32 frames
int counter;
for(counter = 0; counter < 32; counter += 1) // for(initialization; condition; increment)
{
    Link->HP += 2;
    Waitframe();
}

// That's the same as doing this:
int counter = 0; // initialization
while(counter < 32) // condition
{
    Link->HP += 2;
    Waitframe();
    counter += 1; // increment
}

// Count the number of frames before A is pressed
int frameCounter;
for(frameCounter = 0; !Link->InputA; frameCounter += 1)
{
    // The loop body just waits; the increment statement handles the counter
    Waitframe();
}

// Draw a rotating square around Link
int angle;
for(angle = 0; true; angle = (angle + 2) % 360)
{
    // The counter here is used for the rotation angle;
    // it increases 2 degrees each frame and resets to 0 when it reaches 360.
    Screen->Rectangle(6, Link->X - 16, Link->Y - 16, Link->X + 32, Link->Y + 32,
                      Rand(16), 1, Link->X + 8, Link->Y + 8, angle, false, 128);
    Waitframe();
}

// If you leave out the initialization and increment, you're effectively left with a while loop.

// Drains Link's HP until he has one heart left
for(; Link->HP > 16; )
{
    Link->HP -= 1;
    Waitframe();
}
 
One of the most common uses of for loops is to access every element of an array. This is commonly called "iterating over" the array.

// Remember that array elements are numbered from 0 to one less than the array's size.
// This is why it's it's standard practice to start the counter at 0 and go until
// one less than the number of iterations.

// Take every item away from Link
int index;
for(index = 0; index < 256; index += 1) // Runs with index = 0..255
{
    Link->Item[index] = false;
    // No Waitframe() - this is a loop intended to run all at once
}

// Count the number of combos onscreen with flag 100, either inherent or placed
int flag100Counter=0;
for(index = 0; index < 176; index += 1)
{
    // Check both types of flags at once so no combo is counted twice
    if(Screen->ComboF[index] == 100 || Screen->ComboI[index] == 100)
    {
        flag100Counter += 1;
    }
}
 
That's it for now, though there are some more details that will be discussed in the next section. Until then, some sample scripts...

// This script will make the FFC move in a circle around Link.

const float ORBIT_SPEED = 2.5;
const float ORBIT_RADIUS = 48;

ffc script OrbitLink
{
    void run()
    {
        float angle;
        for(angle = 0; true; angle = (angle + ORBIT_SPEED) % 360)
        {
            this->X = Link->X + ORBIT_RADIUS * Cos(angle);
            this->Y = Link->Y + ORBIT_RADIUS * Sin(angle);
            Waitframe();
        }
    }
}
 
// This script will close all open doors when there are no enemies onscreen.

import "std.zh"

ffc script EnemiesHoldDoors
{
    void run()
    {
        int direction; // The direction currently being checked
        int doorState; // This is just used to make it easier to write
        
        // Enemies don't appear for four frames; the script needs to wait for them
        // to appear before waiting for them to disappear
        Waitframes(4);
        
        while(Screen->NumNPCs() > 0)
        {
            Waitframe(); // Just wait until the monsters are gone
        }
        
        // Cycle through the four doors - the directions are 0-3
        for(direction = 0; direction < 4; direction += 1)
        {
            // Make sure the door is open before closing it;
            // you wouldn't want to turn a wall into a door
            doorState = Screen->Door[direction];
            if(doorState == D_OPEN || doorState == D_OPENSHUTTER ||
               doorState == D_UNLOCKED || doorState == D_BOSSUNLOCKED)
            {
                Screen->Door[direction] = D_1WAYSHUTTER;
            }
        }
    }
}
 
And practice exercises.
  • Write a script that finds every instance of flag CF_SCRIPT1 on the screen and changes the combo at that location.
    Spoiler
  • Write a script that rewards Link with money based on how long it took to defeat the enemies on the screen.
    Hint

    Spoiler

Miscellaneous


This section will briefly discuss the third loop type and some miscellaneous information that is relevant to loops and conditionals.



The last type of loop is the do-while loop.

do
{
    <body>
} while(<condition>);
 
This is almost the same as a while loop. The difference is that the first condition check is skipped. In other words, the loop will always run at least once.

Notice that, in this case, a semicolon is allowed after the while statement. ZScript doesn't require it, but C and C++ do, so it's usually used.

The do-while loop is only getting a brief mention here partly because it's just a slight variation on the while loop, but also because it's not used very often. It's mainly useful when you want to do something that might fail and need to be retried. It can also be used when the condition is initially false and will be made true the first time through the loop.

// Maybe you want to pick a random point on the screen, but you don't want it to be
// too close to Link. A do-while loop lets you try again until you get the result you want.
int x;
int y;
do
{
    x=Rand(256);
    y=Rand(176);
} while(Distance(x, y, Link->X, Link->Y) < 32);

// Another place you could use these is for a jump handled by the script rather than the game.
float zVel = 3; // This will track how far Link should move between frames
float zPos = 0; // Link->Z is an integer; use a separate variable to compensate
do
{
    zPos = Max(0, zPos + zVel); // Adjust Link's Z position; don't let it go below 0
    Link->Z = zPos;
    zVel -= 0.16; // Adjust velocity for gravity
    Link->Jump = 0; // Cancel built-in gravity handling
    Waitframe();
} while(zPos > 0); // zPos is 0 at first, but it becomes positive the first time through the loop
 
--------------------------------------------------------------------------------

Normally, a loop body runs from beginning to end over and over until the condition becomes false, but there are two special statements that can alter this.

The break statement makes a loop end immediately. When break is encountered, the rest of the loop's body is skipped, and it does not repeat, regardless of whether the condition is true or false. In a for loop, the increment statement is skipped.

The continue statement ends the current iteration of the loop. It's just the same as reaching the body's closing brace early. The condition will be evaluated, and, if it's still true, the loop will run again. In a for loop, the increment will be evaluated first, as it normally would.

break and continue can be used in any type of loop, but only in loops. They should be in the body of an if statement; it rarely if ever makes any sense to use them unconditionally, aside from debugging.

int counter;
for(counter = 1; counter <= 10; counter += 1)
{
    if(counter == 5)
    {
        // When the counter hits 5, skip the rest of the loop body
        continue;
    }
    else if(counter == 8)
    {
        // When the counter reaches 8, break out of the loop
        break;
    }
    
    Trace(counter);
}
// After that loop finishes, the value of counter will be 8, and allegro.log will show this:
// 1.0000
// 2.0000
// 3.0000
// 4.0000
// 6.0000
// 7.0000

// This will drain Link's HP until A is pressed
while(true)
{
    if(Link->PressA)
    {
        break;
    }
    Link->HP -= 1;
    Waitframe();
}
 
--------------------------------------------------------------------------------

There are two more arithmetic operators that weren't mentioned before: increment and decrement. These are simply shorthand ways of adding and subtracting 1.
  • ++ increment
  • -- decrement
A++ is equivalent to A = A + 1
A-- is equivalent to A = A - 1

These are mentioned now simply because they're most often used in for loops.

// Change every combo on the screen to CSet 0
int index;
for(index = 0; index < 176; index++)
{
    Screen->ComboC[index] = 0;
}

// These are most often used in loops, but they don't have to be.
Link->HP++; // Add one to Link's HP
this->X--; // Move one pixel to the left

// You shouldn't combine increment and decrement with other operations.
// It's legal, but it's confusing and never necessary.
// These are examples of things that you should NOT do:
Link->X = Link->X--;
while(counter++ < 32)
if(this->Y++ == Link->Y--)
 
Technically, there are four of these operators: pre-increment, post-increment, pre-decrement, and post-decrement. The difference is whether they are written before or after the variable.

// These are equivalent
var++;
++var;

// And these are equivalent
var--;
--var;
 
There is actually a small difference between the pre- and post- versions: they have different precedence (that is, their places in the order of operations are different). This is only relevant if you're using them in needlessly confusing ways, so you can consider them equivalent.

--------------------------------------------------------------------------------

When writing for loops, it's possible (and far more common, in fact) to declare the counter variable in the initialization statement. If you do so, the variable only exists until the end of the loop.

// Disable the A button for 60 frames
for(int counter = 0; counter < 60; counter++)
{
    Link->InputA=false;
    Waitframe();
}

counter = 0; // ERROR: counter doesn't exist here
 
--------------------------------------------------------------------------------

When writing loops and conditionals, you don't always need to use braces around the body. If the body is only one statement, they can be omitted.

// Do nothing until A is pressed
while(!Link->InputA)
    Waitframe();

// Play a sound if Link has three hearts or less
if(Link->HP < 48)
    Game->PlaySound(15);

// It's common to see simple conditional statements written in one line.
if(Link->InputStart) break;
 
You shouldn't do this until you're pretty comfortable with the logic. It's easier to make mistakes this way, especially when multiple loops or conditionals are combined. One simple example of how this might cause confusion:

// This doesn't do anything interesting; it's just an arbitrary example.
float dist = Distance(this->X, this->Y, Link->X, Link->Y);
if(dist < 48)
    if(Link->HP == Link->MaxHP)
        Trace(1);
else if(dist > 96)
    Trace(2);

// The indentation is misleading. Since there are no braces, the else is actually
// associated with the second if. The above is equivalent to this:
if(dist < 48)
{
    if(Link->HP == Link->MaxHP)
    {
        Trace(1);
    }
    else if(dist > 96)
    {
        // Obviously, dist < 48 and dist > 96 can't both be true, so this will never run
        Trace(2);
    }
}
 
--------------------------------------------------------------------------------

One final note. The condition of a loop or conditional statement is usually a bool or a Boolean expression, but it doesn't have to be. A number is also legal. 0 is considered false, and any other number is considered true.

// These are equivalent
while(true)
while(1)

// Wait for five frames
int counter = 5;
while(counter)
{
    counter--;
    Waitframe();
}
 
 
 

User-defined functions and script arguments


While rarely if ever strictly necessary, it's often a good idea to write your own functions. Effective use of functions can save you a lot of copying and pasting and make scripts easier to read and maintain.

The syntax should be familiar:

<return type> <name>(<arguments>)
{
    <body>
}
 
A simple example:

float Add(float addend1, float addend2)
{
    return addend1 + addend2;
}
 
The first line is called the header. This is the same as you've seen in zscript.txt and std.txt: first the return type, then the name, then the arguments in parentheses. The parentheses are needed even if the function takes no arguments.

The body of a function is arbitrary code, the same as you've been writing all along. The only thing that's new is the return statement. This ends the execution of the function and returns the specified value. The return value must match the return type. Expressions are evaluated before being returned; for instance, return 2 + 5; will return 7.

// This function returns true if x is greater than y; otherwise, it returns false
bool IsGreater(float x, float y)
{
    if(x > y)
    {
        return true;
    }
    else
    {
        return false;
    }
}

// This function returns true if the given combo is a damage combo
bool IsDamageCombo(int index)
{
    int type;
    
    // Input validation is generally a good idea
    if(index < 0 || index > 175)
    {
        // Out of range
        return false;
    }
    
    // Read the combo type and compare it against every damage type in std_constants.zh
    type = Screen->ComboT[index];
    return type == CT_DAMAGE1 || type == CT_DAMAGE2 ||
           type == CT_DAMAGE3 || type == CT_DAMAGE4 ||
           type == CT_DAMAGE5 || type == CT_DAMAGE6 ||
           type == CT_DAMAGE7;
}

// The return type specified must match what is actually returned.
bool Add(float addend1, float addend2)
{
    return addend1 + addend2; // ERROR: This is a number, not a bool
}
 
As seen in the first example above, return may be used conditionally. It is possible to create a situation in which no return statement is ever reached. The compiler will not catch this, but it should be considered an error.

// A slightly different version...
bool IsGreater(float x, float y)
{
    // If x and y are equal, neither return statement will be executed.
    if(x > y)
    {
        return true;
    }
    else if (x < y)
    {
        return false;
    }
}
 
That doesn't apply to void functions, though. Void functions don't return anything, and it will cause an error if you try. A void function doesn't need a return at all, but you can use it to end the function early. If you do this in run(), it's the same as calling Quit().

// This function will kill Link
void KillLink()
{
    Link->HP = 0;
    // Nothing is returned
}

// This function doesn't do anything
void DoNothing()
{
    return;
    Link->HP = 0; // This line will never run
}
 
Functions can be defined either globally or within a script.

void GlobalFunction()
{
    // A function defined outside of the script is global, which means it can
    // be used by any script. Be sure to give these unique names.
}

ffc script Script
{
    void run()
    {
        // Gotta have run()...
    }
    
    void ScriptFunction()
    {
        // A function defined inside a script can only be used within the script.
    }
}
 
Like global variables and constants, global functions can conflict, so it's usually best to define functions within scripts when possible. Generally, you only need to create global functions if you're making a header file to be used in lots of different scripts (like std.zh) or a global script (discussed later).

Unlike variables, functions can be used before the point in the script where they're defined. As long as they're defined somewhere, the compiler will figure it out.

ffc script SquareNumbers
{
    void run()
    {
        // This will print 1, 4, and 9 to allegro.log
        Trace(Square(1));
        Trace(Square(2));
        Trace(Square(3));
    }
    
    float Square(float num)
    {
        return num * num;
    }
}
 
Numbers and bools passed to functions as arguments will still be the same afterward.

ffc script Arguments
{
    void run()
    {
        int num = 10;
        SetToFive(num);
        Trace(num); // The number printed will be 10
    }
    
    // This function effectively does nothing
    void SetToFive(int x)
    {
        x = 5;
    }
}
 
User-defined pointers are covered in the advanced tutorial, but there's a special case that needs to be addressed here. The special pointer this isn't automatically defined in functions other than run(). If you want to use it in a function, the function needs to take it as an argument. In FFC scripts, its type is ffc; in item scripts, it's itemdata.

ffc script LeftMover
{
    void run()
    {
        while(true)
        {
            MoveLeft(this, 2);
            Waitframe();
        }
    }
    
    void MoveLeft(ffc this, float step)
    {
        this->X -= step;
    }
}
 
Finally, there's the matter of handling script arguments. This is quite simple: they're arguments to run(). run() can take up to eight arguments, which can be either numbers or bools. The first argument corresponds to D0, the second to D1, etc.
ZQuest only allows numbers to be entered. If the argument is a bool, a value of 0 is false, and anything else is true.

ffc script Args
{
    void run(int number, bool trueOrFalse)
    {
        Trace(number); // Traces whatever was entered as D0
        TraceB(trueOrFalse); // Traces false if D1 was 0, true otherwise
    }
}
 
In the case of item scripts, the same arguments are passed to both pickup and action scripts. If both need arguments, it may be necessary to have one of them take the first few as dummies, only actually making use of the higher numbered arguments.

There is no way to input arguments to global scripts. Their run() functions can take arguments, but they will always be 0 or false.

That's all for functions for now. A lot of details, but nothing too tricky. However, given that functions just run the same old code in a slightly different way, you might be wondering when and how you should use them.

The most obvious reason is that if you perform some operation frequently, calling a function is often simpler than retyping the whole thing every time. This is the case with the functions from std.zh. Every function in it is very simple - most of them are under five lines - but calling the Distance() function is easier and clearer than writing out the whole calculation every time you need it.

Another reason to use functions is that they can make scripts much easier to write and understand. If you're making an enemy script, for instance, you might do something like this:

if(LinkInRange())
{
    DoFireballAttack();
}
else
{
    JumpAround();
}
 
Just from the function names, you can get a pretty good idea of what that's meant to accomplish. Such code is called "self-documenting," because the code itself clearly explains what it does.

As with constants, this also makes it easier to modify the script later. If you want to change how the fireball attack works, you only need to change that function. If you want to use it in different circumstances, it's easy to move the function call or add another.

Functions are particularly useful in global scripts. Since there's only one active global script, it's often necessary for users to combine global scripts manually. This is much easier if everything is done in functions. In this case, the functions should be global; again, be careful to use unique names. Also, such functions should almost never include Waitframe() or Waitdraw(), since they will hold up the entire script.

global script CustomGlobal
{
    void run()
    {
        InitIceArrows();
        
        while(true)
        {
            DoCompassSound();
            UpdateIceArrows1();
            
            Waitdraw();
            
            DrawStatusIndicator();
            UpdateIceArrows2();
            
            Waitframe();
        }
    }
}
 
Writing functions for the global script is not always straightforward. You'll often need to use global variables to track data from one frame to the next. For instance, you may need to keep two or three of them to determine if Link's moved since the previous frame. You might need some sort of global counter to track where you are in an ongoing process. This will sometimes make things more difficult for you, but it will be easier for other users (as long as you don't use up all the global variables).

Lastly, one noteworthy type of function you might write is a replacement for Waitframe(). If there's something your script has to do every single frame, you may want to combine that process and Waitframe() into a single function. Usually, this is done in larger scripts that use Waitframe() in more than one place or when the part that needs done every frame is relatively long or complicated.

That's all for this section. Here are a couple of things to try for practice.
  • Write a script that displays one of two messages depending on whether Link has a certain item. Take the item ID and message numbers as script arguments.
    Spoiler
  • Try modifying global scripts from earlier sections to use functions that can easily be combined.
And that's the end of the intermediate tutorial. You'll need to know this stuff pretty well for the advanced tutorial, so spend some time getting comfortable with it before moving on.
  • Rambly, paraquefingir and TheLink like this

#2 David

David

    Fallen leaves... adorn my night.

  • Administrators
  • Real Name:David
  • Pronouns:He / Him

Posted 22 December 2014 - 03:32 PM

Woah. Thanks so much for making this. At first glance, I can see that this will be incredibly useful for those people that are trying to learn how to script. One thing I did notice while looking around was that the links at the top of the post that link to other places in that post don't work except for the first three links. (Never mind, they work now.)

 

Other than that, thanks so much for making this! :)


  • lincolnpepper likes this


0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users