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 screenOr 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?
That's a perfectly good starting point. In pseudocode:
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.
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.
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 LinkNow 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?
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:
And the step-by-step ZScript correspondence:
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.
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:
And again, can each part of that be converted easily to ZScript? Hint: check std.txt for the IP_ constants.
Simple enough. Here's the result:
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.
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.
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:
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:
Simple as that. ZScript equivalents?
And the resulting script:
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.
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:
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:
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.
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:
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.
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:
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 itemAll 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.
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.
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...
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.
And with that done, adding it to the script is easy.
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.