Jump to content

Photo

Intermediate scripting tutorial (WIP)


  • Please log in to reply
19 replies to this topic

#16 Banjo7J

Banjo7J

    Defender

  • Members
  • Real Name:John
  • Location:Scotland

Posted 23 October 2009 - 11:24 AM

Thanks. So, I would add to that like this, right?:
CODE
Game->Counter[CR_RUPEES] += 1;


EDIT:OK, I should stop posting my questions here because I'll have quite a couple.

Edited by Banjo7J, 23 October 2009 - 12:16 PM.


#17 Schwa

Schwa

    Enjoy the Moment more. This strengthens Imagination.

  • Members
  • Real Name:Hunter S.
  • Location:Redmond Subspace (I had a Potion)

Posted 29 November 2010 - 02:59 AM

Hmm, so For loops can be defined without some of their native properties if desired?

Alright, check this out.
CODE

int Step;
for(; true; Step %= (360 + (Step + 1)))
{
  // lots of code
  Waitframe();
}

What I THINK I have here is a For Loop that is set up just like a Permanent While Loop, except every frame it increments a counter-variable by 1 until it reaches 359, and then at 360 it becomes 0 again. This could be used for a variety of circle-based tasks that use all 360 degrees, like making a FFC orbit around a block, or perhaps a rotating Beamos statue that would attack when it sees Link.

But the question is, do I have the mathematical syntax set up right? I think I do, but...

#18 Saffith

Saffith

    IPv7 user

  • Members

Posted 14 December 2010 - 06:13 PM

Ah, sorry. Overlooked this.
Step %= (360 + (Step + 1)) won't do anything. That's setting Step to the remainder of Step/(Step+361), which is just Step again. What you're looking for is Step = (Step + 1) % 360.



User-defined functions and script arguments


In complex scripts, and sometimes even in simple ones, it's often a good idea to write your own functions. While it's rarely if ever strictly necessary, effective use of functions can save you a lot of copying and pasting and make scripts easier to comprehend.

The syntax should be familiar:

<return type> <name>(<arguments>)
{
    <body>
}
A simple example, since that's easier to parse:

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));
    }
    
    int Square(int 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. It's unfortunate that such things are necessary, but when writing scripts for general use, ease of use should be a high priority.

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 this tutorial. Again, spend some time getting comfortable with all this before moving on. Things get a lot more difficult in the advanced tutorial.

The advanced tutorial is here.

#19 Schwa

Schwa

    Enjoy the Moment more. This strengthens Imagination.

  • Members
  • Real Name:Hunter S.
  • Location:Redmond Subspace (I had a Potion)

Posted 15 December 2010 - 10:25 PM

QUOTE(Saffith @ Dec 14 2010, 03:13 PM) View Post

Ah, sorry. Overlooked this.
Step %= (360 + (Step + 1)) won't do anything. That's setting Step to the remainder of Step/(Step+361), which is just Step again. What you're looking for is Step = (Step + 1) % 360.

Oh, sounds good icon_smile.gif I've been having to use Modulus creatively lately, it can be confusing at times. Thanks~

Got another question, this one about Variable Declarations. Right now I have a script that has been screwing up ALL DAY; no matter what I tried, it's like my variables weren't even being read by the game. Finally I ran some Trace() commands, and to my horror, I discovered that this whole time, the two key Local Variables in my Global Script had been resetting to 0 every frame. o_o

Then I realized one possibility: unlike my FFC scripts, this one (which is actually a Function linked from the main Global Script) declares all its Variables outside of any sort of loop-- most of my FFC scripts make use of "while(true)" with a Waitframe() inside, and declares its variables right before then, but this one declares all its variables right after "void <function name>()", which in turn is declared inside my Global Script inside a "while(true)".

So basically that's no different than declaring a slew of unsuspecting variables inside that "while(true)", isn't it!? And they're being re-declared over and over again, every frame, which resets them to 0?

I'm starting to think so, because if you can declare variables inside For Loops successfully like you showed, that implies they're set to 0 whenever they're re-declared (otherwise the loops would only run properly one time, right?)...

I'm going to try fixing this part. If I'm right, I'm going to scream at how stupid I was, and we'll let this be a lesson to everyone reading this to keep track of what your loops really look like. icon_wacky.gif

EDIT - I WAS RIGHT. HA. It works now, after I set them as Global Variables instead. icon_doh.gif Can't believe this cost me over 7 hours of repetitive struggling and it was such an easy fix. LEARN FROM ME, PEOPLE.

Edited by Schwa, 15 December 2010 - 10:36 PM.


#20 Mero

Mero

    Touch Fluffy Tail

  • Banned
  • Real Name:Tamamo No Mae
  • Location:Rainbow Factory

Posted 28 July 2011 - 12:51 PM

So long since anyone posted wow! Hey saffith you think you could go more into Do While loops and talk a little bit more about how break and continue work. I originally thought they had to do with Do while loops which is why I wasn't using them.


1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users