Jump to content

Photo
* * * * * 4 votes

[Scripting/2.50] ZScript tutorial 1 - basic


  • Please log in to reply
4 replies to this topic

#1 Saffith

Saffith

    IPv7 user

  • ZC Developers
  • Gender:Male

Posted 22 December 2014 - 02:27 PM

This tutorial covers basic use of ZScript. It is not written as a general introduction to programming. Instead, it will begin with the simplest functional scripts before moving on to more complex control flow and data manipulation in the intermediate and advanced tutorials.

You should already know how to load and use scripts. If not, that's covered in this tutorial: http://www.purezc.ne...showtopic=38376

By the end of the basic tutorial, you should know the language well enough to write useful scripts, even if only simple ones. However, this is only intended to teach the language itself. Learning how to make good use of it requires experience; it's best to write a lot of practice scripts and experiment to see how things work. Scripting requires a way of thinking that doesn't come naturally to most people, and you won't learn that just by reading a few tutorials.

It's important not rush through this. Each section builds on the ones before; if you move on before you're ready, it'll just make things more difficult.

Comments and style
Getting started
Pointers and variables
Arithmetic
Functions
Constants
Arrays
 
 
 

Comments and style


A few things need to be mentioned before getting into actual scripting.

Comments are notes in the script that are ignored by the compiler. They exist only for the sake of human readers. A comment begins with two consecutive slashes and lasts for the rest of the line.
// This is a comment. The line below is not.
int x=5;
int y=10; // This is a comment, but the first part of the line is not.
 
When you write scripts, it's generally a good idea to add comments to clarify what you're trying to do. They can also be used to temporarily remove ("comment out") code for debugging, but you shouldn't need to worry about that for a while. This tutorial will use comments extensively in code examples to explain what they're doing.

Whitespace is a collective term for spaces, tabs, and line breaks. The compiler doesn't care what you use for whitespace; any combination of the three is equivalent. The only exception is comments; a comment always ends at the end of a line. There are places where whitespace is required, places where it is optional, and places where it is forbidden. These are not really worth going into, as they're pretty intuitive. In general, as long as a human reader wouldn't be confused, the compiler won't, either.

To illustrate, here are three snippets of code that differ only in whitespace:
while (true) {
    Link -> X += 5;
    Waitframe ();
}
 
while(true)
{
    Link->X+=5;
    Waitframe();
}
 
while(true){Link->X+=5;Waitframe();}
 
All three of these are identical, as far as the compiler is concerned.

Since whitespace doesn't matter to the compiler, the best way to use it is strictly a matter of opinion. There are many different ways of handling line breaks and braces. Wikipedia has an article describing several common styles: https://en.wikipedia...ki/Indent_style

Indent style is a very personal matter to many programmers and the subject of numerous flame wars. Different styles have their pros and cons, but all that really matters in the end is that you're comfortable with the style you use. However you like it, though, you should stick to it. Inconsistent spacing looks sloppy and is hard to read, much like English with no punctuation. It's especially important to indent properly when asking for help; if your scripts are hard to read, you'll find people much less willing to search them for errors.

Unlike whitespace, capitalization is important to the compiler. ZScript is strictly case-sensitive. If you try to use link->x instead of Link->X, your script won't work. Names you make up yourself can be capitalized any way you like; again, just be consistent with it.

Finally, a note on terminology. { and } are curly braces or just braces. [ and ] are square brackets or brackets. ( and ) are parentheses. They're all used for different purposes and are never interchangeable, so be sure not to get them confused.
 
 
 

Getting started


Generally, the first thing you should put in a script (after any instructional comments) is this line:
import "std.zh"
 
This tells the compiler to include the contents of the file std.zh with your script. std.zh defines lots of helpful functions and constants, and almost every non-trivial script uses it in one way or another. Although you won't actually need it for a while, it's good to get into the habit of including it. However, when using multiple scripts, std.zh (or any script file, for that matter) should only be included once. Importing the same file more than once will cause errors.

The next thing you have to do is tell the compiler what kind of script you're writing. For an FFC script, you would write this:
import "std.zh"

ffc script ScriptName
{
    // Script body goes here
}
 
This part is pretty straightforward. The phrase ffc script indicates that you're creating, obviously, an FFC script. That's followed by the script's name, in this case ScriptName. There are three types of scripts you can create:
ffc script ScriptName // An FFC script
global script ScriptName // A global script
item script ScriptName // An item script
 
Script names are made up of letters, numbers, and underscores ( _ ). The first character cannot be a number, and the name must not be a language keyword (like script, int, or return). Spaces are not allowed in script names.

Valid Names:
  • CustomBoss2
  • makelinkdostuff
  • Feed_Ganon_six_tacos
Invalid Names:
  • RefillHP&MP // Contains an illegal character (&)
  • int // "int" is a ZScript keyword
  • 3Hearts // The first character is a number
  • ^__^ // Contains illegal characters (^)
The braces and everything in between form the block of code that makes up the script's body. Everything in between those braces is part of the script, i.e.:

ffc script BraceExample
{ // The opening brace of the script
    // The body of the script
} // The closing brace of the script
 
That's all you need to define a script, but there's one more thing you have to add before it will work.

ffc script ScriptName
{
    void run()
    {
        // Code goes here
    }
}
 
This defines the run() function. What exactly this means will have to wait. For now, just remember that the body of void run() is what the game will actually run.

A bit about the different types of scripts.

An FFC script is one that can be attached to a freeform combo. Normally, these will run when Link enters the screen (either before or after the screen scrolls, depending on the "Run Script at Screen Init" flag). It is also possible for a script to set up an FFC to run another script on demand, but this is an advanced technique.

An item script can be attached to an item to run either when the item is picked up or when it is used. Any item script can be used in either way, but almost every script is specifically intended for one use or the other. Item scripts only run for a single frame, so they're commonly used in conjunction with FFC and global scripts to perform actions over time.

A global script is one that is run by the game itself. There are four global scripts. The Active script runs every time the quest is played and can run indefinitely. That's almost always the one you're interested in, but, for the sake of completeness, here are the others. The Init script runs when the quest is started for the first time, and the Continue script runs when it's reloaded after saving and quitting. The Exit script runs whenever Link dies, the game is won, or Game > End Game (F6) is used. The Init, Continue, and Exit scripts can only run for one frame each, so they're not useful for gameplay purposes. You can't load a script into the Init slot manually; instead, to create an Init script, the script must be named Init.
 
 
 

Pointers and variables


Pointers represent game objects, such as Link, FFCs, enemies, or the current screen. Variables represent properties of those objects, such as their HP, position, CSet, or tile. (These aren't really accurate definitions of the terms, but they'll do for now.)

For now, we'll only look at these special pointers:
  • Link - Represents Link
  • this - Represents the FFC or itemdata running the script; this isn't defined in global scripts
  • Screen - Represents the current screen
  • Game - Represents the game itself
Each of these pointers has a number of variables. These are accessed using the arrow operator. For example, Link->HP represents Link's current HP, and this->CSet is the CSet of the FFC running the script.

Variables can be one of two types of data, but there are three different words to identify them.

One type of data is numbers. Numbers are identified with the terms int (short for "integer") or float (short for "floating-point number"). The reason there are two terms will be explained shortly. Numbers go to four decimal places and have a range of -214748.3648 to 214748.3647.

The other data type is bool. Bools have only two possible values: true and false. They represent data you would think of in terms of "yes or no." For example, Link->InputA, which indicates whether the A button is pressed, is a bool. Either the button is pressed (in which case the value is true) or it isn't (false).

The value of a variable is changed with an equals sign. Changing a variable's value is called assignment. An assignment statement must end with a semicolon.
Link->HP = 16; // Set Link's HP to 16 (one full heart)
Link->InputB = true; // Press the B button
Link = 32; // ERROR: You can't assign numbers to pointers directly;
  //you have to set their variables. It doesn't make sense to set Link himself to 32.

// The variable you're setting must always be on the left side of the equals sign.
16 = Link->HP; // ERROR: The compiler thinks you're trying to change the value of the number 16

// You can also assign one variable the value of another.
Link->HP = Link->MaxHP; // Sets the value of Link's HP to the value of his MaxHP, filling Link's HP
this->X = this->X; // Set the FFC's X position to itself - this doesn't do anything, but it's legal
Link->X = Link->InputUp; // ERROR: Link->InputUp is a bool, but this->X is a number.
  // Setting a number to "true" or "false" doesn't make sense.

// Assigning one variable to another is a one-time thing.
// If one changes later on, the other won't change with it.
Link->MaxHP = 48; // Set Link's max HP to 48 (3 hearts)
Link->HP = Link->MaxHP; // Now Link's current and max HP are both 48
Link->MaxHP = 64; // Link's max HP is now 64, but his current HP is still 48
 
To see all of the different variables you can set, look at zscript.txt. The "class" or "namespace" tells you what pointer type something belong to. Those terms aren't meaningful in ZScript; the file is formatted the way it is to work with C++ documentation tools. The ones you should be looking at are under Link, Game, Screen, FFC (for FFC scripts only), and itemdata (for item scripts only).

As an example, one of Link's variables:
 /**
* The time, in frames, until Link regains use of his sword. -1 signifies
* a permanent loss of the sword.
*/
int SwordJinx;
 
The last line tells you the type and name of the variable. For now, if it has any type other than bool, int, or float, or if it has brackets or parentheses after the name, just ignore it. Those will be covered later.

The difference between int and float is that ints are whole numbers (i.e., integers), while floats can have fractional values. An FFC's CSet, for instance, is an int. There isn't, say, a CSet 4.62. This is a mostly a matter of convention; as far as the compiler is concerned, they're just different names for the same thing. However, when assigning a value to an int-type variable, it will usually be rounded down to an integer value.

Note also that variables will not always be the type you expect, and there are a few inconsistencies. For example, Link's X, Y, and Z coordinates are ints, but FFCs' coordinates are floats. FFCs are capable of sub-pixel positioning and movement, but Link is not.

You may notice that some variables have a note that "The effects of writing to this variable are undefined." These are meant to be read-only; you shouldn't try to change them. Assigning new values to these variables probably won't work as you expect, and even if it does, it's not guaranteed to work on every platform or in later versions.

One last note. Hexadecimal (base 16) numbers can be written by prefixing them with 0x. This is mainly useful when dealing with screen numbers.
Game->ContinueScreen = 0x2F;
 
At this point, you know enough ZScript to write useful scripts, albeit only extremely simple ones. Again, you are strongly encouraged to write practice scripts for every new concept. Being a good scripter requires experience, and there's only one way to get it.

Here are some sample scripts using what's been covered so far. For now, FFC scripts will take effect as soon as Link walks onto the screen. If you want to delay them, you'll need to delay the appearance of the FFC (by using a warp, perhaps). Also, these will only run once. If you want to do something again and again, such as every time a button is pressed, you'll need loops, which will be covered in the intermediate tutorial.
// Using an item with this script will refill Link's HP and MP.

item script Recharge
{
    void run()
    {
        Link->HP = Link->MaxHP;
        Link->MP = Link->MaxMP;
    }
}
 
// This script will enable level 4 cheats. This won't work correctly if
// cheats are disabled in the quest.

item script CheatLevel4
{
    void run()
    {
        Game->Cheat = 4;
    }
}
 
// This script will cause the screen to shake for one second and make Link jump into the air.
// It will also disable the sword and items for as long as the screen shakes.

ffc script Earthquake
{
    void run()
    {
        // There are 60 frames in a second, so...
        Screen->Quake = 60;
        Link->Jump = 3;
        Link->SwordJinx = 60;
        Link->ItemJinx = 60;
    }
}
 
Also, here are a few suggestions for practice scripts. While you're not in any way required to complete these, you should be able to do so. If you can't figure out how to do these, you're probably not ready to move on. Possible answers are given in spoiler tags.
  • Write an FFC script that moves Link to the FFC's position.
    Spoiler
  • Write a script that sets Link's HP to 3 full hearts (16 HP per heart) and his MP to 3 full magic containers (32 MP per magic container).
    Spoiler
  • Write an FFC script that makes the FFC move to the left and accelerate to the right.
    Hint

    Spoiler

Arithmetic


You should already be familiar with the arithmetic operations used in ZScript.
  • - Negation
  • + Addition
  • - Subtraction
  • * Multiplication
  • / Division
  • % Modulus
You may not have encountered modulus before, but it's simple enough. A % B (pronounced "A modulo B") gives you the remainder of A divided by B. For example, 23 % 5 = 3. This will be helpful later on for dealing with regular intervals - counting seconds, for instance, or determining whether Link is centered on a tile.

The standard order of operations applies: multiplication, division, and modulus are done before addition and subtraction. You can override this with parentheses; parts of expressions in parentheses are evaluated first. In programming terminology, the order of operations is called operator precedence. Operators with higher precedence are evaluated before those with lower precedence.

You assign a variable the result of an arithmetic operation the same way as a single value.
this->CSet = 1 + 1; // Set the FFC's CSet to 2
Link->MP = Link->MaxMP / 2; // Set Link's MP to 50% of the max
Link->HP = Link->HP + 8; // Add 8 to Link's current HP
this->X = 16 * ((5 + 4) / 3); // Set the FFC's X position to 48; there's no reason to do it this way, but you can
Link->HP + Link->MP = 16; // ERROR: The compiler can't figure this out;
  // arithmetic has to go on the right side of the equals sign

// You can make both numbers and variables negative. To make a variable negative,
// put the minus before the pointer.
Link->HP = Link->HP + -16; // Reduce Link's HP by adding -16
this->Vx = -this->Vx; // Reverse the FFC's X movement by negating its velocity
 
Often, arithmetic is used to adjust a variable's current value (as in the third example above) rather than assign it a completely new value. Because this is so common, there's a shorthand way of doing it by combining the arithmetic and assignment operators. For instance, A += B is equivalent to A = A + B, and A *= B + C is equivalent to A = A * (B + C).

Some examples:

Link->MP += 8; // Equivalent to Link->MP = Link->MP + 8;
this->Vx *= 1.5; // Equivalent to this->Vx = this->Vx * 1.5;
Link->HP -= Link->HP % 16; // Equivalent to Link->HP = Link->HP - (Link->HP % 16);
Link->MP -= -16; // Increase Link's MP by subtracting -16
Game->Cheat += 1; // Increase the current cheat level by 1

// Whatever operation you perform in this way, it's done last,
// regardless of the usual order of operations.
this->Vx *= this->Vy + 3; // Equivalent to this->Vx = this->Vx * (this->Vy + 3);
  // NOT this->Vx = this->Vx * this->Vy + 3;
 
That's all there is to using arithmetic for now, but there's a quirk in division that it's important to be aware of. Because of the way ZC handles numbers internally, the results of division are rounded down, so they won't always be what you expect. This usually isn't a problem, but there are times when seemingly equivalent equations will have completely different outcomes.

Link->HP *= 0.5; // Reduce Link's HP by half
Link->HP /= 2; // Reduce Link's HP by half
Link->HP *= 1 / 2; // Set Link's HP to 0, because 1 / 2 is rounded down to 0
 
If you're familiar with other programming languages, you might expect that you can get the desired result by writing 1.0 / 2.0 instead. That's not the case; in ZScript, it makes no difference.

Once again, here are some complete sample scripts. Arithmetic is a big part of scripting, so be sure you understand how these work.

// This script reduces Link's current HP and MP by 25%.
ffc script SeventyFive
{
    void run()
    {
        Link->HP *= 0.75;
        Link->MP *= 0.75;
    }
}
 
// This script will make the FFC aim at Link, but accelerate in the opposite direction.
ffc script VeerAway
{
    void run()
    {
        // Dividing by 60 means it will take one second to reach Link's position -
        // or it would, if it weren't accelerating away
        this->Vx = (Link->X - this->X) / 60;
        this->Vy = (Link->Y - this->Y) / 60;

        // Acceleration can be unintutive; you may want to try this one out.
        // The acceleration is based on the negative of the velocity, so if it
        // starts out moving up and left, it'll accelerate down and right.
        this->Ax = -this->Vx / 150;
        this->Ay = -this->Vy / 150;
    }
}
 
// This script disables Link's sword for one second for every full heart and
// disables items for one second for every full magic container.
ffc script ProportionateDisability
{
    void run()
    {
        // 16 HP in a heart, 60 frames in a second
        Link->SwordJinx = (Link->HP / 16) * 60;

        // And 32 MP in a magic container
        Link->ItemJinx = (Link->MP / 32) * 60;
    }
}
 
And a couple more practice exercises:
  • Write a script that moves Link one tile (16 pixels) down and right.
    Spoiler
  • Make an item that reduces Link's MP by 25% and increases his HP by 1 for every 2 MP taken. Think it through; this is simpler than it sounds.
    Hint

    Spoiler

Functions


A function is a unit of code that performs a specific task. That's a helpful definition, isn't it? It's hard to come up with a better one, because using, or calling, a function can potentially do anything. Some functions do things in the game, like play a sound or warp Link to another screen. Others give you access to more advanced mathematical operations, such as exponential or trigonometric functions. Some give you information, like how many enemies are onscreen. A function may also just put a complicated procedure into a form that's more convenient to use.

In zscript.txt, functions are the ones that have parentheses after their names. The parentheses may or may not be empty.

Here's an example of a simple mathematical function:

/**
* Returns the result of addend1 + addend2
*/
float Add(float addend1, float addend2);
 
This isn't a real function, but it would be easy to create.

The first part after the description, float, is the function's return type, the type of data you'll get from using it. A function can return a number, a bool, or a pointer. Some functions don't return anything at all; these have the special return type void.

After the return type is the name. When talking about functions, it's common, but not necessary, to include an empty pair of parentheses. You could refer to this function as either Add or Add(). In code, however, the parentheses are always required.

Inside the parentheses are the arguments. These are inputs, data that will be used by the function to decide what to return or what to do. The arguments are listed in the usual way: type, then name. If a function requires multiple arguments, they're separated by commas. This function takes two arguments, both numbers. The names of the arguments are irrelevant when you call the function; they're only there to give you an idea of what they're used for.

Here's how you would use this function.

// All the function does is add two numbers, so it's equivalent to ordinary addition.
// You assign the return value to a variable the same way as anything else.
this->X = Add(5, 11); // Equivalent to this->X = 5 + 11;
Link->HP = Add(48, -16); // Equivalent to Link->HP = 48 + -16;
Add(10, 25) = this->Y; // ERROR: You can't assign a value to a function
Link->HP = Add(4, 12, 8); // ERROR: The function takes exactly two arguments; you can't use more or fewer, even if it seems logical

// If a function returns a number, you can use it in arithmetic, too
Link->Z = 5 * Add(1, 3); // Equivalent to Link->Z = 5 * (1 + 3);
Link->HP += Add(8, 8); // Equivalent to Link->HP = Link->HP + (8 + 8);

// You can pass in variables as arguments.
this->Y = Add(this->X, 32); // Equivalent to this->Y = this->X + 32;
Link->MP = Add(Link->MP, Link->MP); // Equivalent to Link->MP = Link->MP + Link->MP;

// You can use arithmetic operations. They'll be evaluated first, then the results will be used as arguments.
Link->HP = Add(Link->HP, 2 * 16); // Equivalent to Link->HP = Link->HP + (2 * 16); - The parentheses are implied
this->X = Add(5 * (2 + 4), 7 - 5); // Equivalent to this->X = (5 * (2 + 4)) + (7 - 5);

// You can even pass the results of other function calls.
Link->X = Add(Add(4, 8), 17); // Equivalent to Link->X = (4 + 8) + 17;
Link->MP = Add(Add(Add(12, 4), 32), Add(Link->HP / 16, 24)); // That's a bit much, but it's legal
 
In zscript.txt, you'll see global functions listed at the top, followed by those associated with different pointer types. Global functions are called using just their names, like the Add() function above. For the others, you use the pointer and arrow, just like with variables. A couple of examples of real functions:

Link->HP = Max(Link->HP, Link->MaxHP * 0.5); // Set Link's HP to 50% full or his current HP, whichever is greater
this->Data = Game->GetComboData(1, 0, 0); // Set the FFC's combo to that at the top-left corner of map #1
 
Void functions work just by being called. Since they have no return value, it'll cause an error if you try to assign one to a variable. You still need a semicolon at the end of the line.

Game->PlaySound(16); // Play sound effect #16
Link->Warp(12, 0x44); // Warp Link to screen 44 on DMap 12
ClearTile(10442); // Deletes tile 10442
 
When you call a function, variables that you pass in as arguments will not change.

Link->MaxHP = 64;
Link->HP = Sqrt(Link->MaxHP); // Link's HP becomes 8; his max HP is still 64
 
That's about all you need to know about using functions for now, but there are some special global functions that are worth singling out.

So far, every script has executed instantly, all in a single frame. To make scripts run over a period of time, you need the Waitframe() function. It'll pause the script for one frame, then pick up where it left off 1/60 of a second later. This doesn't work in item scripts or non-active glocal scripts, since they only run for one frame; the script will simply stop executing when it reaches the Waitframe().

Waitdraw() is only used in the active global script. Each frame, the global script is run before most of the frame's regular updates are done. Enemies haven't moved yet, Link hasn't started swinging his sword yet, and so forth. Waitdraw() suspends the execution of the script until after all of this has been done and just before the screen is redrawn.

The Trace() and TraceToBase() functions write a number to the allegro.log file. Similarly, TraceB() takes a Boolean argument and writes either "true" or "false." TraceNL() writes a blank line. These functions are used mainly for debugging.

Trace(Link->HP); // After the script runs, a line in allegro.log will tell you what Link's HP was at the time
 
There's also TraceS(), which writes a string, but that's a much more advanced topic.

The Quit() function makes the script stop running. There's no reason for you to use this yet.

Link->X += 5;
Quit();
Link->X -= 5; // This line will never run
 
One last note: one of the reasons for importing std.zh is that it brings in the contents of std_functions.zh, which defines many useful functions. These functions are documented in std.txt. All functions in std.zh (and any other .zh file) are global. A few of them worth highlighting:

void Waitframes(int n)
* Calls Waitframe() n times.

void NoAction()
* Unpresses all buttons except start and map

void WaitNoAction()
void WaitNoAction(int frames)
* Combines NoAction() and Waitframe().

bool IsSideview()
* Returns true if the current screen is a side-view screen.

int Rand(int min, int max)
* Returns a random integer between min and max.

float Randf(float n)
float Randf(float n1, float n2)
* Returns a random floating-point (i.e. non-integer) number up to n or between n1 and n2.

float Clamp(float x, float low, float high)
* Returns x if it's between low and high, or low or high if not. Used to limit
* a number to a certain range.

float VectorX(int len, float angle)
float VectorY(int len, float angle)
* Used for movement at an arbitrary angle. Returns the values for X and Y velocity
* needed to move len pixels at angle degrees.

int ComboAt(int x, int y)
* Converts screen coordinates to a combo position. This will be very useful soon.
 
That's it for functions for now. For the time being, just look at those functions that return int, float, bool, or void. Some of the more complex ones may look intimidating, but if you understand the concepts, you'll be able to figure them out.

Now, some sample scripts...

// This script will play sound effect #27 (the secret sound, by default).

ffc script SecretSound
{
    void run()
    {
        Game->PlaySound(27);
    }
}
 
// This script will draw a quickly shrinking circle around Link on layer 5.
// You'll learn better ways to do this sort of thing later on.

ffc script CircleLink
{
    void run()
    {
        // Don't be overwhelmed by all the arguments. Just figure out what they do one by one.
        Screen->Circle(5, Link->X+8, Link->Y+8, 16, 3, 1, 0, 0, 0, false, 128);
        Waitframe();
        Screen->Circle(5, Link->X+8, Link->Y+8, 12, 3, 1, 0, 0, 0, false, 128);
        Waitframe();
        Screen->Circle(5, Link->X+8, Link->Y+8, 8, 3, 1, 0, 0, 0, false, 128);
        Waitframe();
        Screen->Circle(5, Link->X+8, Link->Y+8, 4, 3, 1, 0, 0, 0, false, 128);
    }
}
 
// This script uses trigonometric functions to make the FFC aim at Link, moving at one pixel per frame.

import "std.zh"

ffc script MoveTowardLink
{
    void run()
    {
        // Using functions from std.zh:
        this->Vx = VectorX(1, Angle(this->X, this->Y, Link->X, Link->Y));
        this->Vy = VectorY(1, Angle(this->X, this->Y, Link->X, Link->Y));
        
        // Here's another way to do it.
        // For angular movement at a given speed, X velocity is speed * Cos(angle),
        // and Y velocity is speed * Sin(angle).
        //this->Vx = 1 * RadianCos(ArcTan(Link->X - this->X, Link->Y - this->Y));
        //this->Vy = 1 * RadianSin(ArcTan(Link->X - this->X, Link->Y - this->Y));
    }
}
 
And some practice suggestions.
  • Make an FFC script that waits for ten seconds and then plays a sound.
    Spoiler
  • Write a global script that sets the cheat level higher for every ten times Link has died, but don't let it go above 4.
    Hint

    Spoiler
  • Write a script that waits for five seconds and then makes the screen flash.
    Hint

    Spoiler

Constants


Now you know how to play a sound...

Game->PlaySound(3);
 
Game->PlaySound() makes enough sense, but 3? 3 isn't a sound, it's a number. That's one of the reasons constants exist:

Game->PlaySound(SFX_BOMB);
 
Much better. This is the second reason for importing std.zh. It includes std_constants.zh, which defines the SFX_ constants and many others.

A constant is simply a number with a name. When the compiler sees SFX_BOMB, it replaces it with 3. It does exactly the same thing as before, but it makes it easier to understand by using a meaningful name instead of an arbitrary number.

You can also define your own constants.

const float CONSTANT_NAME = 12.3456;

ffc script ScriptName
{
    // Constants must be defined outside of the script itself.
    const int NO_GOOD = 0; // ERROR
}
 
To indicate that you're naming a constant, start a line with const. That's followed by the constant's data type. However, constants have to be numbers, so the only valid types are int and float. After that is the name, and then you specify the value as if you were setting a variable.

Legal names for constants are the same as legal names for scripts: any combination of letters, numbers, and underscores that does not start with a number and is not a ZScript keyword. Constants are normally named using all caps, sometimes with underscores between words. They commonly have a prefix indicating their purpose; std.zh uses the SFX_ prefix for sounds, for instance. Constant names must be unique; if you reuse a name from std.zh or another script in the same quest, it'll cause an error. Therefore, it's a good idea to give the names a prefix based on the name of the script that uses them.

The constant's value, as mentioned before, must be a number. You can't assign a constant the value of a variable or a function's return value. That's because the values are set when the script is compiled, and variables and functions can't be evaluated until the script is run. Furthermore, you can't set a constant's value to another constant, and the value can't be the result or an arithmetic operation.

// These are invalid.
const int SQRT_20 = Sqrt(20); // ERROR: Can't use the result of a function call
const int LINKS_MAX_HP = Link->MaxHP; // ERROR: Can't use variables
const int SOUND_TO_PLAY = SFX_BOMB; // ERROR: Can't use another constant
const int TWENTY = 2 * 10; // ERROR: Can't use arithmetic
 
Once you set a constant's value, it can't be changed by the script. That's why they're called constants.

import "std.zh"

ffc script Example
{
    void run()
    {
        SFX_OUCH = 10; // ERROR: The compiler will interpret this as 19 = 10;
    }
}
 
There's another reason to use constants besides making scripts more readable. They also make it easier to change how your script works. It's simpler to change the value of a constant than dig through the code to find some arbitrary number, especially if your script is large and complex or if the same number is used in several different places. This can also allow quest makers to customize your scripts without having to figure out how they work.

That's it for this section. Nothing too hard, just another way to write numbers. A couple of sample scripts...

// This script plays the secret sound effect.
import "std.zh"

ffc script SecretSound
{
    void run()
    {
        Game->PlaySound(SFX_SECRET);
    }
}
 
// This script will play a sound and make the screen shake.
// It will also disable the sword and items for as long as the screen shakes.

// Change the constants to control how long the quake lasts and what sound is played.
const int EQ_QUAKE_TIME = 60;
const int EQ_QUAKE_SOUND = 13;

ffc script Earthquake
{
    void run()
    {
        Game->PlaySound(EQ_QUAKE_SOUND);
        Screen->Quake = EQ_QUAKE_TIME;
        Link->SwordJinx = EQ_QUAKE_TIME;
        Link->ItemJinx = EQ_QUAKE_TIME;
    }
}
 
// This warps Link to DMap 10, screen 1F.
const int WARP_DEST_DMAP = 10;
const int WARP_DEST_SCREEN = 0x1F;

ffc script WarpLink
{
    void run()
    {
        Link->PitWarp(WARP_DEST_DMAP, WARP_DEST_SCREEN);
    }
}
 
For practice, try modifying earlier sample scripts to use constants.
 
 
 

Arrays


An array is like a lot of variables all put together in a list. They're used to represent lots of data that are logically grouped together. Data in an array are all of the same type and handled in the same way, and, in most cases, they all serve basically the same purpose. Link's inventory, for instance, is an array of bools. There's one bool corresponding to each possible item, which is true if Link has that item and false if he doesn't. All of these bools are combined into the array Link->Item[].

Each of the entries in the array is called an element. The position of an element in the array is called its index. You would use index 5 to access the element Link->Item[5].

Working with the elements of an array is much like working with ordinary variables. The only difference is that you must specify an index; you can't change the entire array at once.

Link->Item[10] = true; // Give Link item #10
Link->Item[5] = false; // Take away item #5
Game->LKeys[3] += 2; // Give Link two additional keys for level 3
Game->LKeys = 5; // ERROR: You have to specify a single array element
 
The number of elements in an array is its size or length. For technical reasons, the index of the first element is 0, and the index of the last element is one less than the array's size. The size of Link->Item[] is 256, since the game allows 256 items; the first element is Link->Item[0] and the last is Link->Item[255].

When using arrays, be careful to use valid indices, because the compiler can't check them. If you try to set Link->Item[300], it will successfully compile, but there's nothing sensible it can do.

Anything that gives you a number can be used for an array index.

Link->Item[5 + 5] = true; // Arithmetic is fine
Link->Item[Link->X] = false; // Variables work, though this one doesn't make much sense
Game->LKeys[Sqrt(64)] = 4; // Functions are fine, too
Link->Item[Game->LKeys[3]] = true; // Even array elements work
 
In some cases, it's obvious what number to use for the index. In Game->LKeys[], for instance, the index corresponds to the level number. Other times, however, the index is arbitrary. Each element of this->Flags[] represents one of the FFC's flags, but you couldn't guess which index goes to which flag. std.zh provides lots of constants for exactly this reason. Just look at zscript.txt to see which set of constants to use for a given array.

Link->Item[I_SWORD2] = true; // Give Link the White Sword
this->Flags[FFCF_TRANS] = true; // Make the FFC transparent
Screen->Door[DIR_UP] = D_UNLOCKED; // Unlock the door at the top of the screen
Game->Generic[GEN_HEARTPIECES] += 1; // Give Link a Heart Container Piece
 
That's really all there is to using arrays, but there's a set of them that warrants some elaboration. All of the combos on the screen can be accessed via array members of Screen. Screen->ComboD[] represents the ID of each combo, Screen->ComboC[] gives you the CSets, and so forth. The combos are numbered from 0 to 175, like so:

[000][001][002][003][004][005][006][007][008][009][010][011][012][013][014][015]
[016][017][018][019][020][021][022][023][024][025][026][027][028][029][030][031]
[032][033][034][035][036][037][038][039][040][041][042][043][044][045][046][047]
[048][049][050][051][052][053][054][055][056][057][058][059][060][061][062][063]
[064][065][066][067][068][069][070][071][072][073][074][075][076][077][078][079]
[080][081][082][083][084][085][086][087][088][089][090][091][092][093][094][095]
[096][097][098][099][100][101][102][103][104][105][106][107][108][109][110][111]
[112][113][114][115][116][117][118][119][120][121][122][123][124][125][126][127]
[128][129][130][131][132][133][134][135][136][137][138][139][140][141][142][143]
[144][145][146][147][148][149][150][151][152][153][154][155][156][157][158][159]
[160][161][162][163][164][165][166][167][168][169][170][171][172][173][174][175]
 
These numbers aren't always convenient to work with, so std.zh defines the ComboAt() function to convert from x,y coordinates.

Screen->ComboC[ComboAt(Link->X, Link->Y)] += 1; // Change the CSet of the combo Link is standing on
this->Data = Screen->ComboD[ComboAt(this->X, this->Y)]; // Turn the FFC into the combo at its position
 
Be careful when working with ComboT[] (combo type), ComboI[] (inherent flag), or ComboS[] (solidity). Unlike the other combo arrays, the data these arrays represent are part of the combo definitions. Changing these will affect every instance of the same combo in the entire quest.

That's it for arrays. As usual, a few sample scripts to end with.

// This script will take away Link's swords.

import "std.zh"

ffc script Disarm
{
    void run()
    {
        Link->Item[I_SWORD1]=false;
        Link->Item[I_SWORD2]=false;
        Link->Item[I_SWORD3]=false;
        Link->Item[I_SWORD4]=false;
    }
}
 
// This script refills Link's bombs and increases the maximum by 4.

import "std.zh"

ffc script MoreBombs
{
    void run()
    {
        Game->MCounter[CR_BOMBS] += 4;
        Game->Counter[CR_BOMBS] = Game->MCounter[CR_BOMBS];
    }
}
 
// When this item is used, it will randomly change the CSet of the combo Link is standing on.

import "std.zh"

item script ColorChange
{
    void run()
    {
        Screen->ComboC[ComboAt(Link->X, Link->Y)] = Rand(12);
    }
}
 
And one last pair of practice exercises:
  • Make an item that gives Link a key and reduces his max HP by one heart. Be sure not to take all his life, though.
    Hint

    Spoiler
  • Give Link an extra bomb for every heart he's missing from his max HP.
    Spoiler
That's the end of the basic tutorial. Nothing too difficult yet, but get some practice before moving on. The intermediate tutorial is significantly harder.

You're sure to have questions along the way; whenever you can, try finding the answers yourself rather than asking on the forums. It's good practice, and you'll probably get your answer sooner.
  • Rambly, NewJourneysFire, Astromeow and 1 other like this

#2 TheBlueTophat

TheBlueTophat

    Don't be an Asshat. Be a Tophat!

  • Members
  • Gender:Male
  • Location:USA

Posted 22 December 2014 - 02:40 PM

So you have made a more polished version of the original tutorials some? That is quite good, I must say. Must of taken awhile to accomplish. Good job!



#3 NewJourneysFire

NewJourneysFire

    Deified

  • Contributors
  • Real Name:Grant
  • Gender:Male

Posted 22 December 2014 - 05:54 PM

I'm glad to see these back. I'll be returning to studying these regularly now that they have returned. 



#4 accela2me

accela2me

    Illustrious

  • Members
  • Real Name:Juliano
  • Gender:Male
  • Location:Minas Gerais, Brazil

Posted 22 December 2014 - 06:06 PM

Thanks so much for this (new) tutorial, Saffith! Excellent work :)



#5 Astromeow

Astromeow

    RESPECT DA OBOE SOLO

  • Members
  • Gender:Unspecified

Posted 01 October 2015 - 10:04 AM

This topic is gold. Professor Saffith I enjoyed your coverage and practice examples!




1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users