Jump to content

Photo

Writing scripts


  • Please log in to reply
11 replies to this topic

#1 Saffith

Saffith

    IPv7 user

  • Members

Posted 11 November 2017 - 08:02 PM

This is meant to be sort of a guide for those of you who've made some headway in learning the language but struggled to do anything useful with it. If you don't know the language at all, you obviously won't be able to follow the examples so well, but maybe it'll still be of interest. Not sure if this'll actually be helpful to anyone, but I tried, at least.

As I see it, there are basically three different aspects of scripting you have to learn to deal with: code, logic, and interacting with the game. These are to some degree inseparable, but they are distinct.



Code

I've already written tutorials covering the language, but there are a couple more things to say about it.

First of all, there's one practice I want to make sure to discourage. Don't write comments like this:
// Do this for values of i from 0 to 175
for(int i=0; i<176; i++)
{
    // Check the combo at position i, and if it's combo 241...
    if(Screen->ComboD[i]==241)
    {
        // Then change it to combo 247
        Screen->ComboD[i]=247;
    }
}
These comments aren't clarifying the code; they're just translating it. The uncommented code is perfectly clear if you understand the language. Comments explaining basic syntax encourage you not to focus on the code. It's sort of like trying to learn Japanese by watching subtitled anime. If you focus on the language you already know and tune out the one you don't, you're not going to learn much.

When learning any language, programming, natural, or otherwise, there's a period where you have to translate it in your head to understand it. You can't be fluent until you grow out of this - that is, until you learn to understand the language as it's written or spoken. This isn't really something that can be taught; you just have to work at it. The important thing is not to shy away from it. Just read the code and figure out what it means. It may be hard at first, but this more than anything else is just a matter of practice. This isn't to say that you need to think in code, just that you have to learn to express your thoughts that way. Of course, you also have to think the right thoughts, but that's another matter.

You should be aware that ZScript, like natural languages, has idioms. There are a lot of common operations that require multiple steps in code, and seeing these as the higher-level operations they represent is essential. For instance...
for(int i=1; i<=Screen->NumLWeapons(); i++)
{
    lweapon wpn=Screen->LoadLWeapon(i);
    // ...
}
This is used to do something with every lweapon on the screen. You can figure that out by reading each individual step, but it's so common that you should try to interpret it as a single unit.



Logic

Whatever your script does, you need to spell out every step in excruciating detail, but thinking about things that way is practically impossible. The key here is abstraction - looking at a series of small steps collectively, as a single, larger action.

This is something you're undoubtedly good at already. Consider the process of going to the grocery store. If you broke that into a sequence of smaller steps, would it look like this?
Open the door
Step outside
Close the door
Walk to the car
Unlock the car door
Open the car door
Get in the car
Close the car door
Start the car
Put the car in reverse
And so on...

Of course not. You may have to do all those things, but you'd never get anywhere if you thought like that. More likely, you'd come up with something like this:
Get in car
Drive to store
Go into store
Buy things
Return to car
Drive back home

Writing a script is very similar. It may not be as familiar - there will be times when you overlook important steps, or things just don't work the way you think they do - but, in general, this is something you already know how to do. You just have to learn to apply it properly.

Generally, this means splitting things into functions. Suppose you're making an enemy with this attack pattern:
Wait three seconds between attacks
If the charge attack can reach Link, use it
Otherwise, shoot fireballs

In ZScript, that might look like this:
while(true)
{
    Waitframes(180);
    if(LinkInRange(this))
        Charge(this);
    else
        ShootFireballs(this);
}
It's easy to see that the code does what the description says. This makes the code not just easier to read, but also easier to write. You can focus on the logic at a higher or lower level as needed; you don't have to think about calculating the movement angle or the timing and spread of fireballs while working on the overall pattern. It also makes it easier to change things later. If you want to change how the fireball attack works, all the code you have to update is in one place, and you generally won't have to mess with the rest of the script.

How far you should go with this is somewhat subjective, and you'll learn by experience what you're comfortable with. But in general, I think it's harder to take it too far than not far enough.

On the other hand, what about something like this?
bool Activate(ffc this)
{
    if(LinkIsStandingOn(this) && LinkIsFacingUp() && PlayerPressedA())
        return true;
    else
        return false;
}
At this point, it's no longer abstraction. This is just another way of translating ZScript to English. There are sometimes good reasons to write functions for individual, simple expressions, but if you're doing it because PlayerPressedA() is easier to read than Link->PressA, don't.

Another skill that's important to learn is rethinking your logic in different but equivalent ways. If you don't see how to express something in code, a little reworking may make it clear. "Until X happens" is equivalent to "while X has not happened." "Unless X is true" is the same as "if X is not true." Notice that unless and until are not meaningful terms in ZScript, but if, while, and not are. Sometimes, it's necessary to make a process more complicated or indirect. Rather than "for each EW_FIREBALL on the screen, do X," you want "for each eweapon on the screen, if it is an EW_FIREBALL, do X." There isn't any built-in way to get a list of fireballs, but there is a list of all eweapons and an easy way to tell which ones are EW_FIREBALLs. This is the reason many of those idioms exist: there are a lot of simple, common procedures that take a little more work than just calling a function.

Always think your logic through carefully and be sure every possibility is handled correctly. For instance, if you have something like this:
bool Activate(ffc this)
{
    if(this->Misc[ENABLED]==1)
    {
        if(Link->PressA)
            return true;
    }
    else
        return false;
}
What happens if this->Misc[ENABLED] is 1 and A isn't pressed? What value is returned? That possibility isn't accounted for, and neither return statement executes in that case. That's an error the compiler won't catch. Sometimes it won't have any effect, and sometimes it'll completely break your script.



Interacting with the game

At some point, any script needs to make something happen. While this is generally simple conceptually, actually doing what you're trying to do may not be. This is most often where you deal with stuff you "just have to know," and where the answer to "why doesn't this work?" will be "it just doesn't."

Want to position an FFC on the screen? Just set its X and Y variables. Simple as can be, works exactly as you'd expect.

Want to simulate hitting Link? That's trickier. You can adjust Link->HP, set Link->Action to LA_GOTHURTLAND, and set Link->HitDir to whatever direction, but that isn't perfect. Rings won't reduce damage if you set Link->HP directly, and it won't matter if Link is invincible due to a clock or having been hit by something else recently. You could also create an invisible eweapon and put it on top of Link. That uses the engine's built-in handling, so it will work perfectly. However, you still need to account for the possibility that Link is invincible. If the weapon doesn't hit him, it will stay where you put it, creating an invisible hazard for the player to stumble into later.

If there isn't a function or variable that does what you want to do, or if it doesn't work quite the way you want it to, you just have to use what's available to fake it. In some cases, getting the game to do some seemingly trivial thing will require substantial integration with the script's overall logic. In a script that alters the game's built-in behavior, it may well be that 99% of the script's logic is working around the game's quirks. Such scripts are generally the most difficult to write; they require a good understanding of the game's behavior and a lot of trial and error.

Relatedly, it's occasionally important to remember that whatever script you're writing, it's really only a simulation. If you're writing an enemy script, you're not really scripting an enemy; you're just telling an FFC how to pretend to be an enemy. An FFC has its own behavior to factor in. In particular, it always starts in the same state, with no memory of anything that happened when it ran before. If you want a scripted switch to stay triggered when Link leaves and returns, you'll have to make that happen yourself.

Often, you'll only learn how things work by experimenting, so get used to that. I strongly recommend setting up a simple test quest for trying things out easily. Just a simple screen and a minimal global or FFC script, like:
global script TestStuff
{
    void run()
    {
        while(true)
        {
            if(Link->PressEx1)
            {
                // Do something interesting
            }
            Waitframe();
        }
    }
}
Whenever there's something you want to try, just add it in there, and you can see what happens in a matter of seconds.



Pseudocode

A brief explanation of pseudocode, in case you're not familiar. Peudocode is simply writing that is structured like code, but isn't. It's useful for working through the logic of a script and figuring out how everything fits together without worrying about the details of the language - essentially, a rough draft of a script. I'll be doing this a lot in the examples below.

There's no formal specification of pseudocode. It can be plain English arranged differently:
If all enemies are defeated
    Open doors
    Quit
Otherwise
    Count down timer
    If timer is at 0
        Warp back to previous screen
Or it can be much more code-like:
for i=0..175
{
    if ComboT[i]==CT_SCRIPT1
    {
        PlaySound(SFX_SECRET)
        ComboD[i]++
    }
}
You don't have to worry too much about doing it properly. You write it only for your own benefit, so whatever you're comfortable with is fine. What's important is that it actually corresponds to ZScript. Basically, that means code that runs repeatedly (for each, until, repeat forever, etc.) or conditionally (if, else, unless, etc.) are indented appropriately, as in the examples above.



Examples

I encourage you to work along with these on your own, hence spoiler tags. Hopefully, you can follow along okay even if you don't come up with the same answers. Here's a quest file already set up for these examples: https://www.dropbox....cripts.zip?dl=0
Use PracticeScripts.qst. PositionCheck.qst is an example that I'll explain when it's relevant.

We'll be using a "divide and conquer" strategy here. In short, break the problem down into steps, fill in what you can, and break down the rest into even smaller steps.

First, let's write a treasure chest script. It will take an item number as argument D0.

In the simplest possible terms, what does a chest do?
Spoiler

That's a perfectly good starting point. In pseudocode:
Spoiler

That's good enough for now. Don't worry about how these things work; this is the level of detail we're working at right now.

Now, what about translating this to ZScript? The general idea is to do the simple bits now and put off the more complex parts until later.

Try looking at it this way. Break the pseudocode down to the smallest parts you can, and for each one, ask: does this have an obvious, straightforward analog in ZScript? That is, can this be interpreted as a while loop, a for loop, or an if/else statement? Can this be done by using an existing variable or function? Or is there a common idiom that does this? If you're not sure, just say no. It's easy to change it later.

Spoiler


For each one you said "yes" to, well, there you go. Each "no" will become a function. If that function deals with a number - how many X are there? How far is X from Y? - it should return a number. If it represents a yes-or-no question other than a number comparison, it will return a bool. A function that finds something, like a specific enemy, will return the appropriate type. A function that just does something won't return anything (void). These aren't hard and fast rules, mind, just starting points.

Whether Link is trying to open the chest is a yes-or-no question, so that becomes a function that returns a bool. Giving Link an item just does stuff in the game, so that won't return anything.

With that in mind, let's turn it into code. The parts that converted easily to ZScript can just be put directly in there. The others will become function calls, and we'll add stubs - placeholder functions that don't do anything except maybe return a value - so the code will compile.
Spoiler


Before moving on, how can we be sure this is correct? It compiles and runs, but it doesn't do anything. If there's a logic error, there's no way to tell at this point, because it has no effect.

What we'll do is fill in the functions with placeholder behaviors. They don't need to work right, or even be halfway sensible; they just need to do something so you can tell they're being called at the right times. Let's change what the functions do in the pseudocode to make them trivial:
Until A is pressed
    Wait
Move Link
Now it's easy to fill the functions in.
 
ffc script TreasureChest
{
    void run(int itemID)
    {
        while(!OpenChest())
        {
            Waitframe();
        }
        
        GiveLinkItem();
    }
    
    bool OpenChest()
    {
        // TODO
        return Link->PressA;
    }
    
    void GiveLinkItem()
    {
        // TODO
        Link->X+=16;
    }
}
And now you can verify that it works. It will do nothing until A is pressed, at which point it will move Link a tile to the right. Also, it will only do this once, not every time the button is pressed, meaning the chest doesn't open repeatedly.

Even though we don't plan to keep these little bits of code, this is an important step. You want to catch errors as early as possible. If the script doesn't work the way you expect it to at this point, you've made a mistake, and it should be sorted out right away. The sooner you find an error, the easier it is to fix. If you don't find out about a problem until the whole script is finished, there are a lot more things that could have gone wrong and have to be rechecked.

Now we need to start filling in those functions with the correct behaviors. Let's start with OpenChest. Remember, this was answering a yes-or-no question: is Link trying to open the chest? If yes, the function will return true. If no, it will return false.

How do we know Link is trying to open the chest?
Spoiler

There are two different ways to approach this:
If Link is in front of the chest and Link is facing up and A was pressed, yes; otherwise, no
If Link is not in front of the chest or Link is not facing up or A was not pressed, no; otherwise, yes

It doesn't really matter which you pick. The first is more intuitive, so I'll do that here. The pseudocode is pretty much the same text with different spacing:
Spoiler


And the step-by-step ZScript correspondence:
Spoiler


The case of checking whether Link is standing in front of the chest is a little iffy. It's not a single comparison, but four simple and very similar ones: is Link far enough left? Far enough right? Up? Down? You could make it a separate function, but it usually wouldn't be. Position checking is one of those common idioms. It generally looks about like this:
if(Link->X >= left && Link->X <= right && Link->Y >= top && Link->Y <= bottom)
{
    // Link is in position
}
Or, conversely:
if(Link->X < left || Link->X > right || Link->Y < top || Link->Y > bottom)
{
    // Link is not in position
}
To explain a bit further, left is the farthest left Link can be and still be in position - in other words, the lowest X value allowed. If Link->X is less than that, he's too far left. Similarly, bottom is the farthest down he can be, or the maximum Y value. If Link->Y is greater, he's too far down.

Figuring out what left, right, top, and bottom are may look a bit tricky at first, but it's just a matter of working through them one by one. Individually, they're simple.

Link can be a little bit to the left or right of the chest - half a tile, let's say. Half a tile left of the chest is this->X-8. If Link is farther left than that - if his own X value is less than that - he can't open the chest. Half a tile right is this->X+8; if Link->X is greater than that, he's too far right. With the small hitbox, Link will be half a tile down from the chest if he's right up against it - that's this->Y+8. We'll say he can be another half tile down from there, which is this->Y+16. Plugging in these numbers gives us:
if(Link->X >= this->X-8 && Link->X <= this->X+8 && Link->Y >= this->Y+8 && Link->Y <= this->Y+16)
If you have trouble visualizing the logic here, load up PositionCheck.qst and walk around a bit. It illustrates what each individual condition is doing. You can also press A to display lines indicating the values being compared.

With that, we can fill in the OpenChest() function easily. Don't forget that you now have to pass this as an argument.
Spoiler

This works as it should. If you press A while Link is in front of the chest and facing it, he'll be moved a tile to the right.

I generally prefer to split up long conditions like that. They can get hard to read, especially when you have && and || used together. This one's not too bad, though.

Now what about giving Link an item? Like I said before, setting Link->Item[x]=true doesn't always work, so what do you do instead? Using the game's built-in handling for collecting an item is generally the best option. It's simple, and you know it will always work correctly. All you need to do is create an item right where Link is standing so he'll pick it up automatically. We'll also have him hold the item up; this can be done by setting a flag on the item itself.

Pseudocode is trivial:
Spoiler

And again, can each part of that be converted easily to ZScript? Hint: check std.txt for the IP_ constants.
Spoiler

Simple enough. Here's the result:
Spoiler

Actually, it could be even simpler with std.zh's CreateItemAt(), but whatever.

It basically works now. Link can walk up to the chest and open it, and he'll get an item and hold it up. But there's a problem: the chest's appearance doesn't change when it's opened. This is simple to fix; it's just a matter of changing the FFC's combo. In this case, the open chest graphic is hidden behind the FFC, so it can be shown by making the FFC invisible (i.e. setting it to combo 0). I won't even bother with pseudocode for such a trivial addition.
Spoiler

You could also put it in OpenChest() just before returning true if you think it makes more sense that way. Doesn't really make any difference.

Now the chest now works pretty much perfectly... Except for one major problem. If you open the chest, then leave the screen and return, the chest will be closed, and it will be possible to get the item again.

Remember, this isn't really a treasure chest, but an FFC pretending to be a chest. An FFC starts in the same state every time you enter the screen. If it needs to start in a different state because of something that happened before, that has to be incorporated into the script.

Before anything else, the chest needs to check if it's already been opened. Let's go back and add that into the pseudocode.
Spoiler

Simple enough, but how do you actually make that happen? This is a common problem, and there aren't many ways to solve it, so you learn pretty quickly how to handle it. The usual way is to set some variable when the thing happens and check it at the beginning of the script. For something that's specific to the screen, this would probably be Screen->D[]. However, we can also use Screen->State[], which conveniently has an element indicating whether the chest on the screen has been opened. If Screen->State[ST_CHEST] is true, it's been opened already. That does limit you to one chest per screen, but I'm okay with that.

That means we'll remember that the chest has been opened by setting Screen->State[ST_CHEST] to true, and we'll check if it was opened before by reading that value. The resulting script:
Spoiler

There's one more thing you have to do: check the "Run Script At Screen Init" flag in the FFC's properties. Otherwise, the script won't start and the chest won't reopen until the screen finishes scrolling.

That's all for this script. If you did it yourself and came up with something a bit different, that's probably fine.

Again, when doing the "can this be trivially translated to ZScript?" step, just say no if you're not sure. For instance, if you didn't know how to check Link's direction, you might have done this:
if(Link->X>=this->X-8 && Link->X<=this->X+8 &&
   Link->Y>=this->Y+8 && Link->Y<=this->Y+16 &&
   LinkIsFacingUp() &&
   Link->PressA)
As a temporary measure, that's perfectly fine. You should come back later and replace it once you see it's too simple to justify a function.


---------


Let's do another one, a bit more advanced this time. The FFC will be a fireball shield. You'd most likely use a script like this in conjunction with an item script, but here it's just placed right on the screen.

Here's the simple description of what it will do: the FFC will move around Link in a circle and destroy any enemy fireballs (but not other weapons) that touch it.

Again, the first thing to do is break this down into steps. But it's just one continuous action, so how do you do that? In these cases, think about what the script needs to do each frame.

Pseudocode:
Spoiler

Simple as that. ZScript equivalents?
Spoiler

And the resulting script:
Spoiler

Pretty simple so far. I went ahead and added this arguments. One function moves the FFC and the other reads its position, so they'll clearly be needed.

Let's do the movement first. How do you determine what the next position is? There's no getting around it: this will require some math.

It's not really that complicated, but if you haven't done this before, your first instinct may be wrong (or at least harder than what I'm going for here). That is, you might be inclined to think of it from the FFC's perspective. For clockwise movement, if it's above Link, it moves right; if it's to Link's right, it moves down; if it's below and to the left, it moves up and left...

There's an easier way to handle it. When you're dealing with movement of one object relative to another, consider the relationship between them and how it changes over time. In the case of circular movement, you often need to think about the angle and distance to the center of the circle. In this case, the distance from the FFC to Link is fixed, and the angle changes at a constant rate. That's pretty simple, right?

We'll need to keep track of the angle, and that means a variable. Let's add that into the pseudocode first.
Spoiler


We'll say the angle starts at 0 and increases by 8 degrees per frame. That will result in one circle every 3/4 second. The resulting code:
Spoiler

I went ahead and passed it into SetPosition, too.

Now, how do we get from a center point, distance, and angle to X and Y cordinates? It's actually not that hard to begin with, but there are a couple of functions in std.zh that make it even easier: VectorX and VectorY. These return the X and Y offsets to add for a given distance and angle. They're useful both for circular movement and for moving something at an arbitrary angle.

For circular movement, the general formulation is:
X = center X + VectorX(distance, angle)
Y = center Y + VectorY(distance, angle)
The center is Link, of course, and let's say the distance is 24. The resulting code, then:
Spoiler

Actually, it's simple enough that I'm not going to make it a separate function after all. But there's nothing wrong with leaving it this way if you prefer.
Spoiler


Now, as for destroying the fireballs. We just need to check each fireball to see if it's touching the FFC and remove it if so. Pseudocode:
Spoiler

Remember what I said before about how sometimes you have to make a simple process more complicated? This was the exact example I used. As for destroying the fireball, check out weapon->DeadState in zscript.txt. It's not the clearest thing, but deleting weapons is probably all you'll ever use it for.
Spoiler


There are collision checking functions in std.zh, but I don't want to use those right now. Let's do that the long way.

When are two objects colliding? When one object is not to the left, to the right, above, or below the other. Each has to be far enough left, right, up, and down relative to the other. The general form is:
if(object 1 right >= object 2 left && object 1 left <= object 2 right && object 1 bottom >= object 2 top && object 1 top <= object 2 bottom)
{
    // Objects are colliding
}
Does that look familiar? It's basically the same as the code used to check when Link is standing in front of a treasure chest. Both of these are just variations on the same problem - checking the relative positions of two objects - so naturally the formula is pretty similar. And like before, it looks daunting, but each individual number is simple.

We'll say object 1 is the fireball and object 2 is the FFC, and we'll assume each is one tile. Each object's left is its X variable, and its right is X+15 (not 16 - that's one pixel beyond the right). The top is Y, and the bottom is Y+15. Plugging those numbers in, we get:
if(fb->X+15 >= this->X && fb->X <= this->X+15 && fb->Y+15 >= this->Y && fb->Y <= this->Y+15)
There's a lot of variation in how these things are written, but whenever you see two X comparisons and two Y comparisons with && or || in between, it's a safe bet that it's some sort of relative position check.

We've got all the bits of code worked out, so now we just need to put them all together:
Spoiler


And that's it. Works perfectly.


---------


One more example. Let's add on to an existing script this time.

That treasure chest script before worked fine, but it was extremely basic. Let's add another feature to it: an option to make it require a key. This will be done with argument D1, meaning the signature of run() will become:
void run(int itemID, bool locked)
D1 is actually presented as a number in ZQuest, of course. When an argument to run() is a bool, 0 is false and any other number is true.

First of all, how does this change fit into the pseudocode? Here's what we had at the top level before:
If previously opened
    Open
    Quit
Until Link opens chest
    Wait
Remember that chest was opened
Give Link item
All of that is still right. The main change has to do with opening the chest. This was the relevant pseudocode:
If Link is in front of the chest and Link is facing up and the player pressed A
    Yes (return true)
Else
    No (return false)
This is no longer correct. It's going to have to become a bit more complicated. We have to ask, does Link need a key? If so, does he have one? We'll also need to remove a key if needed.

It's a little tricky to see how to fit this into pseudocode correctly, so let's go back to plain English. How would you explain how this should work? There are several different ways it could be stated. For instance:

- If Link is standing in front of the chest facing upward and the player pressed A, Link is trying to open the chest. If the chest isn't locked, the chest opens. If it is, check if he has a key. If he does, take a key, and then the chest opens.

- If the chest is not locked, it opens if Link is standing in front of it facing upward and the player pressed A. If it is locked, the chest opens and Link loses a key if he has a key, he's standing in front of the chest facing upward, and the player pressed A.

- If Link is standing in front of the chest facing upward, the player pressed A, and either Link has a key or the chest is unlocked, the chest opens. If the chest was locked, Link also loses a key.

These are logically equivalent, and any of them will work. However, they're all a bit hard to parse, so you might prefer to split it up. Think it through, and you should realize we're actually asking two different questions at once here.
Spoiler

Much easier to understand, right? Moreover, that first question is exactly what the function is already checking. With that in mind, it's a lot easier to see how to fit in into the pseudocode, at least at a high level.
Spoiler


How do we actually implement this? The first part is already done. We just need to expand the bits inside that if block. It's not too hard, but there's one important detail you need to remember...
Spoiler

It took a while to figure it out, but in the end, this isn't too complicated. ZScript equivalence is pretty straightforward; just look through zscript.txt and std_constants.zh to figure out where keys are stored. The two kinds of keys are in totally different places.
Spoiler

And with that done, adding it to the script is easy.
Spoiler

That'll do it. There are a lot of different ways you might have done that, and you might very well have put the lock check and key usage in a new function. That's perfectly fine. However you do it, just be careful that it's correct; you wouldn't want to remove a key when the chest is unlocked, for instance.



That's it

The most important thing I want to get across here is that scripting is more about logic than code. Code is just how you explain your logic to the game. That's certainly not to say the code itself is never a challenge - it has its quirks, and not everything you might want to do is straightforward and obvious - but, in general, you should approach scripting thinking "what am I trying to do?" rather than "how do I tell it to do this?" If your logic is clear and correct, you can usually implement it without too much trouble.

I know I'm making this look easy because I already know how to do things. If you didn't know how to delete those fireballs, for instance, it's not the easiest thing to figure out. Some things you just have to know. Look through the documentation and play around with things you don't understand. Like I said before, you should keep a test quest handy to try things out quickly.

Anything you can't figure out, you can always ask on the forums. Someone probably knows. When you do ask questions, try to keep them simple and focused as much as possible. Something like "how can I tell if Link just got hit?" is a lot more likely to be answered than "How do I make a beamos?"

It's important always to remember that things can be changed. Don't worry about getting everything right the first time. Even if you do manage to screw something up beyond repair, you can just start over and know better next time. I've done that more than a few times myself. Likewise, you don't have to get all of a script's features in place on the first pass. Focus on the basic functionality first, and then you can add on to it later. Even the biggest and most complex scripts start small.

And don't get discouraged because there are problems you can't solve. Look at it like a puzzle game. You don't start with the hard ones; you start with the easy ones and work up to harder ones as you learn how to do things. Of course, even recognizing which ones are the hard ones takes some experience. All the more reason not to get discouraged.

If anyone wants to see any more examples, feel free to ask. Nothing too big, please - these take a while to write up.
  • 4matsy, Rambly, Anthus and 3 others like this

#2 Anthus

Anthus

    Deified

  • Members
  • Location:Dark Ohio

Posted 11 November 2017 - 08:39 PM

I will definitely give this a read later (I'm a bit exhausted after three ten hour days in a row). That script you made for me in my thread was a good learning example. When I look at scripts, I can usually tell, to some effect, what it is doing, or "saying". That's a far cry from two years ago, when I had -no idea- at all. It's just that I don't remember it all cause I don't practice consistently. Thanks for taking the time to write this long thing out. :)



#3 Timelord

Timelord

    The Timelord

  • Banned
  • Location:Prydon Academy

Posted 12 November 2017 - 01:05 AM

That's itThe most important thing I want to get across here is that scripting is more about logic than code.

That is indeed, the most critical thing for anyone to learn. A solid logic foundation, and planning out the path that you want, is the only way to do this type of thing; and anyone who takes another approach is effectively, doomed.

I'm one of those people who will have step-by-step commenting, that you detest, but I plan out my code logic using those comments, rather than with pseudocode; so, first I write out a procedure (using code comments), then I flesh out the syntax around them. This style can be very useful when short-term memory is an issue, and you want constant reminders on what you are doing. Ultimately, that one is a style choice.

Readable identifers are absolutely the way to go. I see far too many code bits with nonsensical identifiers, and almost entirely unreadable syntax.

Another critical component, is proper use of Traces, and paying attention to logged errors. You might want to give thought to explaining the process of debugging to the average user, should you continue this.

In general, when I'm doing something complex, I put traces in at various stages of logical progression:

bool bar()
{
    int s[]="We've reached bool bar() and are running it";
    TraceS(s);
    return (Link->PressA);
}

ffc  script f
{
    void run()
    {
        bool a = bar();
        int s[]="bool bar() returned:  ";
        TraceS(s); TraceB(a);
     }
}

Edited by ZoriaRPG, 12 November 2017 - 01:09 AM.


#4 Avaro

Avaro

    o_o

  • Members
  • Real Name:Robin
  • Location:Germany

Posted 12 November 2017 - 11:45 AM

One thing, I'd love to split things into functions like you did there, to better be able to see the logic at a higher level 

while(true)
{
    Waitframes(180);
    if(LinkInRange(this))
        Charge(this);
    else
        ShootFireballs(this);
}

but an issue is that functions don't have access to variables outside of them. Would be really nice if they could.


Edited by Avataro, 12 November 2017 - 11:51 AM.


#5 Saffith

Saffith

    IPv7 user

  • Members

Posted 12 November 2017 - 02:45 PM

There are ways to deal with that. The simplest, of course, is to pass it in as an argument and return a new value if needed. When that's not adequate, use ffc->Misc. It's a bit uglier, but it's the closest thing ZScript has to instance variables.
 
void Update(ffc this)
{
    this->Misc[IDX_TIMER]--;
    if(this->Misc[IDX_TIMER]==0)
    {
        DoWhatever(this);
        this->Misc[IDX_TIMER]=60;
    }
}
If you still need more, you can use arrays. An array can be used to effectively get multiple return values from a function:
ffc script Test
{
    void run()
    {
        int arr[2];
        GetValues(arr);
        Trace(arr[0]); // Traces 5.0000
        Trace(arr[1]); // Traces 10.0000
    }

    void GetValues(int out)
    {
        out[0]=5;
        out[1]=10;
    }
}
And you can even store the array pointer in ffc->Misc so all you have to pass around is this.
  • Avaro likes this

#6 Anthus

Anthus

    Deified

  • Members
  • Location:Dark Ohio

Posted 24 November 2017 - 07:20 PM

I'm liking the idea of writing out a script like a series of instructions to help frame what kind of script you are going to be writing. This is a type of outlining that I never even thought about, but this could be really helpful with helping people find logical errors before they get too far in.

 

So I was trying to think of a way to "outline" a script for a fast travel system. I decided there are easier ways to do it without scripting, such as just using two warp tiles, with blocks around them that require certain items to get passed. It's not quite as original, or clean as my idea below, but I kind of abandoned it for now cause I saw a critical error in my ways of attempting it. See if you can spot it. :P

 

Read this if you are board

Spoiler



#7 Saffith

Saffith

    IPv7 user

  • Members

Posted 24 November 2017 - 08:21 PM

I'm not certain I'm understanding correctly, but it sounds like the thing to do would be to check if the next screen's secrets have been triggered, no? If so, warp there, and if not, back to the first one?

You could also give each one a unique ID number in D1 and use a global variable to keep track of the highest number that's been activated. If it's higher than the current one, the next warp is active.

#8 Saffith

Saffith

    IPv7 user

  • Members

Posted 14 February 2020 - 04:27 PM

Bringing this back up after... More than two years? Wow. Anyway, the post is too long to edit, and there's something I think it's important to add.

Looking back at all that, I think I gave the impression there that writing good scripts is a matter of meticulous planning; that if you work through the steps properly, scripting is an orderly and straightforward process. That is emphatically not the case. The walkthroughs there should be taken as exercises to learn the general thought process: "What does this script do? It does this, this, and this. Okay, how does it do those things?" In practice, it's much messier. You'll be figuring a lot of that stuff out as you along and going back to fix it after you screw it up the first dozen times. Truthfully, the more detailed your planning is, the more likely it is that you'll overlook something and it'll all fall apart.
 
Asaf Hanuka offered this advice to writers:

How to write a good story:

1. Write a bad story
2. Fix it


That applies to many things, scripting among them. Unless you're writing a script so simple that you can do it all in your head, you're not going to nail it from the start. Far more often, you'll write messy, poorly thought out code and fiddle with it until it works. Making it clean and efficient and well-documented comes later. Just don't forget to actually do that part, or you'll be sorry when you come back to it later.

I feel like this is important to emphasize particularly because I struggle with it a lot myself. I have a bad habit of thinking a project is unsalvageable garbage when it really just needs some refactoring. It's easy to compare your work to that of experts and find it wanting, but you have to remember that the things you're seeing from those experts are generally finished products, with no evidence of the failures and frustration along the way.

I wrote tango.zh from scratch three times before the first public alpha, and it was only even that easy because I already knew what I was doing. And even after a fair amount of cleanup:
$ grep -R TODO ./*
./common.zh:// TODO unused?
./common.zh:    // TODO Should __TANGO_FLOW_END_MARKER be included?
./functions.zh:    // TODO Sort by numeric value
./functions.zh:        return TANGO_DEFAULT; // TODO This isn't really correct, but is there a valid case where it might fail?
./loading.zh:        else // TODO decimal points
./metrics.zh:// TODO: Pretty sure these two are unnecessary...
./update.zh:                     // TODO How should functions that produce printable characters be handled?
./user.zh:    // TODO
Alpha 1 was 1509 functional (non-blank, non-comment) lines of code, and version 1.0 was 3360 - and that's not counting the font definitions and demo. It's gone through a lot of revision, and there's still a lot I'd like to improve in the latest version.

Anyway, I don't mean for this to be a motivational speech. Just want to make it clear that the process isn't as tidy as I may have made it seem.
  • Rambly, Evan20000, Hari and 1 other like this

#9 Evan20000

Evan20000

    P͏҉ę͟w͜� ̢͝!

  • Members
  • Real Name:B̵̴̡̕a҉̵̷ņ̢͘͢͜n̷̷ę́͢d̢̨͟͞
  • Location:B̕҉̶͘͝a̶̵҉͝ǹ̵̛͘n̵e̸͜͜͢d҉̶

Posted 15 February 2020 - 05:27 AM

I'm working on one of the most organizationally-challenged boss scripts I've ever made with all sorts of ugly array pack/unpack functions for moving lots of data between different scopes. I couldn't have done something like this 2+ years ago. I'm only able to do this because of constant practice and failure. And I'm not sure the horrific mess I've got now even constitutes success. These tutorials (and Ghost itself!) have been beyond helpful turning people with 0 programming experience into people who kinda know what they're doing, and for that I offer nothing but gratitude.


  • Anthus, Avaro and Hari like this

#10 Timelord

Timelord

    The Timelord

  • Banned
  • Location:Prydon Academy

Posted 16 February 2020 - 03:14 AM

[...]
but you have to remember that the things you're seeing from those experts are generally finished products, with no evidence of the failures and frustration along the way. [...]

 
(emphasis, mine)
 
This is effectively the case the majority of the time, with the exception being repos or disclosed unfinished products of experimental stuff.  There is often room to improve, even finished product. In general,figuratively speaking, I paint broad strokes over a sketch, then work in details and refinements. It is rare that I accomplish anything close to perfection on a first go, and even when I feel that I am close, I often go back anyway to improve upon it.
 
People should not be discouraged by this process.

#11 Geoffrey

Geoffrey

    Chosen One

  • Members

Posted 01 November 2021 - 05:55 PM

Four years later, I wanted to thank you for writing this. It's probably been the single most helpful thing for me so far.

 

If you had the time and the inclination, I think a similar write-up just looking at different 'idioms' would be really useful. That was the point where I really got how to start making things work, and more examples would be really helpful.



#12 Saffith

Saffith

    IPv7 user

  • Members

Posted 16 August 2022 - 08:12 PM

And it only took me another nine months to see it. :appropriateemoji: Glad it was helpful. I do worry a bit about these things being too tidy or too simple.

If you had the time and the inclination, I think a similar write-up just looking at different 'idioms' would be really useful.

I think that would basically be a cheat sheet. Certainly something that could be done, but I'm a bit behind on ZScript these days. Might be a job for someone else at this point, but we'll see.


1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users