If you aren't using it already, you should enable the quest rule "Log Script Errors To Allegro.log" and check for errors after running scripts. There are a lot of mistakes you can make here, and you may not even notice you have without seeing these error messages.
User-defined pointers
User-defined arrays
Strings
Bitwise operations
Scope and function overloading
User-defined pointers
To interact with most types of objects in the game, you'll need to define your own pointers. Declaring a pointer is much like declaring any other variable, except, of course, that the data type is different.
npc enemy; // An npc (enemy) pointer ffc anFFC; // An ffc pointer
There is an important difference with numeric and Boolean variables, however: pointers cannot be global.
lweapon globalWpn; // ERROR ffc script Blah { void run() { lweapon localWpn; // OK } }
Here are the different pointer types and what they represent:
- link - Represents Link (Link is the only pointer of this type)
- screen - Represents the current screen (Screen is the only pointer of this type)
- game - Represents the game (Game is the only pointer of this type)
- ffc - Freeform combos
- npc - Enemies, guys, and fairies
- lweapon - Any of Link's weapons on the screen (including things like explosions and sparkles)
- eweapon - Any enemy weapons on the screen
- item - Any items on the screen
- itemdata - The definition of an item
Pointers are declared the same way as numeric and Boolean variables, but the options for initializing them are more limited. To set a number, you can simply enter the number, like int x = 5;. With pointers, you can't do that; there's no way to enter an item directly in text. Instead, you must either use a function that returns the correct pointer type or another pointer of the same type.
npc enemy = Screen->LoadNPC(1); // Load enemy #1 npc enemy2 = enemy; // enemy and enemy2 point to the same npc enemy = Screen->LoadNPC(5); // Now enemy points to npc #5; enemy2 still points to #1
By now, you may have gotten used to counting from 0, but pointer types don't work that way. If there are three items on the screen, they're numbered 1 to 3, not 0 to 2.
item it = Screen->LoadItem(0); // This will never load a valid item if(Screen->NumItems() == 5) { it = Screen->LoadItem(5); // This is fine; there are 5 items, so 5 is valid }
Keep in mind that pointers refer to objects in the game. All pointers to a given object will see changes made to it.
npc en = Screen->LoadNPC(3); npc en2 = en; en->HP = 10; // Now en2->HP is also 10, because it's the same enemy
Similarly, when passing pointers as arguments to functions, the pointer will refer to the same object afterward, but its properties may have changed.
ffc script EnemyExample { void run() { Waitframes(4); // Wait for enemies to appear npc en = Screen->LoadNPC(3); ModifyEnemy(en); // After that function call, en still points to npc #3, but it now has 10 HP. } void ModifyEnemy(npc en) { en->HP = 10; en = Screen->LoadNPC(5); } }
So far, this is all pretty straightforward. Here's where it gets tricky: in most cases, the objects these pointers represent are transient. There aren't always any enemies, items, or weapons present, and when there are, they eventually go away. What happens if you try to load item #5, but there is no item #5? If you've got a pointer to an enemy, what happens when the enemy dies? When using a pointer, you need to be sure it's valid - that is, that it refers to an object that actually exists.
Before using a Screen->Load* function, you need to make sure the object you want is really there.
item it = Screen->LoadItem(5); // If there aren't 5 items onscreen, the pointer won't be valid npc enemy; if(Screen->NumNPCs() >= 3) { enemy = Screen->LoadNPC(3); // This is fine - there are at least 3 npcs, so npc #3 exists }
If a pointer is valid, it is guaranteed to remain valid until the next Waitframe() or Waitdraw(). After that, the object might be deleted, so you have to check its isValid() method to make sure it's still there.
item it; if(Screen->NumItems() >= 5) { it = Screen->LoadItem(5); // OK - there is an item #5 it->X = 80; // Still OK Waitframe(); it->Y = 32; // Not safe - Link may have just picked up the item, making the pointer invalid } if(it->isValid()) { it->Y = 32; // OK } // A valid pointer will stay valid at least until the next Waitframe() or Waitdraw(), // no matter what you do to it. eweapon ew; if(Screen->NumEWeapons() >= 1) { ew = Screen->LoadEWeapon(1); ew->DeadState = WDS_DEAD; // This tells the weapon to be deleted... ew->X = 80; // but it won't actually happen until Waitframe() is reached, ew->Y = 128; // so you can safely use the pointer until then. }
You don't just have to load existing objects; you can also create new ones. There is a limit to how many of each type of object can exist, however, which means it's possible the creation function will fail and return an invalid pointer. The limits are high enough that you can safely ignore them most of the time, but it may be worth checking in extreme cases.
// If there are 256 eweapons onscreen already, this will fail. eweapon wpn = Screen->CreateEWeapon(EW_SCRIPT1); if(!wpn->isValid()) { // Must be a bullet hell boss Quit(); }
One last, non-obvious validation issue. Let's say you've got an npc pointer, and you want to know when the enemy dies. You'll consider the enemy dead as soon as its HP hits 0, but the pointer will still be valid for several frames as its death animation is displayed. However, it's also possible that another script will kill the enemy by moving it far offscreen, in which case the pointer will suddenly become invalid without the enemy's HP changing. In order to determine if the enemy has died, then, you have to check both its HP and whether the pointer is valid. How do you do that? Your first thought might be something like this:
if(!enemy->isValid() || enemy->HP <= 0)
That seems logical, but there's a problem with it. ZC will always evaluate the whole condition. If isValid() returns false, the game will still check enemy->HP, making use of the invalid pointer.
The best way to do it is with a function that checks each possibility separately.
bool IsDead(npc en) { if(!en->isValid()) return true; if(en->HP <= 0) return true; return false; }
All this validation may look incredibly cumbersome, but it's really not that bad. It's only needed when loading or creating objects and after Waitframe() or Waitdraw(). In cases where a lot of repetitious validation is needed, you can often use functions to minimize the burden. For instance, if you're writing a script that uses Waitframe() in several places, you might make a function like this:
void MyWaitframe(ffc this, npc enemy) { Waitframe(); if(!enemy->isValid()) { this->Data = 0; Quit(); } }
Use MyWaitframe() instead of Waitframe(), and you'll never have to worry about your pointer's validity again.
After all these warnings about validating your pointers, you're probably wondering just what happens if you use an invalid pointer. The answer is: nothing much. ZC will detect the problem, give you some meaningless number (probably -1) if you tried to read a variable, and report an error in allegro.log. The main concern is that your script probably won't work correctly if you don't know when your pointers are valid. And, of course, you don't want people using your scripts to find a lot of errors logged as a result.
ffc and itemdata pointers are exceptions to all of the above. There are always 32 FFCs on the screen, numbered 1-32. Even if they're blank, they're there. An itemdata pointer represents the definition of an item, which isn't even an object in the game. These two can be loaded without checking for their existence, and they don't even have isValid() functions.
ffc anFFC = Screen->LoadFFC(16); // OK - FFC #16 always exists itemdata id = Game->LoadItemData(27); // OK - ItemData #27 always exists
With pointers that do represent onscreen objects, the numbers used to load them depend on the order in which they were added to the screen, and those numbers change when lower-numbered objects are removed.
eweapon wpn1; eweapon wpn2; wpn1 = Screen->LoadEWeapon(5); Waitframe(); wpn2 = Screen->LoadEWeapon(5); // wpn1 and wpn2 may not point to the same eweapon. If any of eweapons 1-5 were // removed during the Waitframe(), a different eweapon will be in slot #5.
Pointers can be compared with the == and != operators. These check whether the two pointers refer to the same object.
lweapon wpn1 = Screen->LoadLWeapon(1); lweapon wpn2 = Screen->LoadLWeapon(2); if(wpn1 == wpn2) // This will evaluate to false, even if the weapons are identical
That'll do it for this section. A few sample scripts...
// This script randomizes the HP of every enemy on the screen import "std.zh" item script EnemyHPRandomizer { void run() { npc enemy; // This is a very common way of iterating over every enemy on the screen for(int counter = 1; counter <= Screen->NumNPCs(); counter++) { enemy = Screen->LoadNPC(counter); enemy->HP = Rand(1, 20); } } }
// This item script fires a fast-moving arrow in the direction Link is facing. import "std.zh" item script Arrow { void run() { lweapon arrow = Screen->CreateLWeapon(LW_ARROW); // Again, you can usually skip the validity check with CreateLWeapon(), // but it certainly doesn't hurt to make sure. if(!arrow->isValid()) { Quit(); } arrow->X = Link->X; arrow->Y = Link->Y; arrow->Dir = Link->Dir; arrow->Step = 500; arrow->Damage = 4; // Adjust the arrow's tile for the direction // Up is default, so it's skipped if(arrow->Dir == DIR_DOWN) { arrow->Flip = 2; } else if(arrow->Dir == DIR_LEFT) { arrow->Tile += 1; arrow->Flip = 1; } else if(arrow->Dir == DIR_RIGHT) { arrow->Tile += 1; } } }
//This script makes the FFC follow Link around and removes any enemy weapons // that get close to him. The FFC should be 2x2 tiles. import "std.zh" ffc script Barrier { void run() { eweapon wpn; while(true) { this->X = Link->X - 8; this->Y = Link->Y - 8; // Cycle through every enemy weapon on the screen for(int counter = 1; counter <= Screen->NumEWeapons(); counter++) { wpn = Screen->LoadEWeapon(counter); if(Distance(Link->X, Link->Y, wpn->X, wpn->Y) < 20) { wpn->DeadState = WDS_DEAD; } } Waitframe(); } } }
You can probably think of plenty of things to do for practice, but here are some suggestions anyway.
- Write a script that spawns enemies at regular intervals if there aren't too many onscreen already.Spoiler
- Modify the Barrier script above so that it protects an enemy instead of Link. Make sure it disappears after the enemy dies.HintSpoiler
- Write an FFC script that causes an arrow to leave a fire when it disappears.HintSpoiler
item script RunFireArrow { void run() { ffc scriptRunner = Screen->LoadFFC(1); scriptRunner->Data = 1; // Combo 1 should be invisible scriptRunner->Script = 1; } }
User-defined arrays
If you need to keep a bunch of data in a form that can be handled in a loop, or if you just have a whole lot of data with the same basic meaning, you should store it in an array. Declaring an array differs from declaring a variable only in that you normally must specify the size.
int intArray[5]; // Creates an array of five ints, intArray[0] - intArray[4] eweapon ewArray[3]; // An array of three eweapons, ewArray[0] - ewArray[2] // An array can be huge. The limit is 214747 elements, the largest number // ZScript can handle. int bigArray[100000]; // This is perfectly fine // The size must be a simple integer. int anArray[x]; // ERROR: Variables can't be used int anotherArray[ARRAY_SIZE]; // ERROR: Constants can't be used int yetAnotherArray[5+5]; // ERROR: Arithmetic can't be used
Number and bool arrays can be global, but pointer arrays can't.
int intArrayGlobal[10]; // OK lweapon lweaponArrayGlobal[5]; // ERROR ffc script Example { void run() { int intArrayLocal[5]; // OK npc npcArrayLocal[10]; // OK } }
To initialize an array, put the initial values in braces, separated by commas.
int intArray[5] = { 1, 2, 3, 4, 5 }; // Creates intArray and sets intArray[0]=1, intArray[1]=2, etc. // This works for pointers, as well. eweapon ewArray[3] = { Screen->LoadEWeapon(3), Screen->LoadEWeapon(4), Screen->LoadEWeapon(5) }; // You don't have to initialize every element. int piArray[10] = { 3, 1, 4, 1, 5 }; // Elements 0-4 will be initialized to these numbers; 5-9 will default to 0 // But don't try to initialize more elements than are in the array. // The compiler won't catch it, but it clearly doesn't make sense. int badArray[3] = { 5, 4, 3, 2, 1 }; // There aren't five elements in the array // Using an array initializer allows you to leave out the array size. The size // will be the same as the number of elements it's initialized with. int autoIntArray[] = { 0, 0, 0, 0 }; // This one will have four elements // This is only valid for initializing the array. You can't do it later on. int intArray2[5]; intArray2 = { 6, 7, 8, 9, 10 }; // ERROR
Here's the first tricky part. After declaring int myArray[10], you should think of myArray (with no brackets) as a pointer to an array rather than a regular variable. However, ZScript doesn't see it that way. To the compiler, there's no difference between an int[] and an int.
int myArray[10]; // As usual, you should assign data to the individual elements... myArray[0] = 1; myArray[1] = 5; // Be careful - assigning a value to the array pointer is legal! myArray = 20;
Assigning a new value to an array pointer changes what array it points to. If you're lucky, the pointer will become invalid, the game will hang, and you'll find a ton of errors in allegro.log. If you're not so lucky, the array pointer will refer to a different array, and your script will completely break with no indication as to why. Be very careful not to do this; it can be one of the hardest mistakes to find.
What if you want to pass an array as an argument to a function? The compiler sees an int array pointer as a regular int, so that's how you should handle it. The same is true of other data types. Passing an array pointer is the same as passing a single variable.
Also, like object pointers, an array pointer won't change when you pass it to a function, but the contents of the array may change.
ffc script ArrayArgs { void run() { int idArray[5] = { 20, 21, 21, 32, 32 }; npc enemyArray[5]; CreateNPCs(idArray, enemyArray, 5); } void CreateNPCs(int ids, npc enemies, int arraySize) { for(int counter = 0; counter < arraySize; counter++) { enemies[counter] = Screen->CreateNPC(ids[counter]); } } }
Notice that there's no indication in the function header that the arguments should be arrays. Be sure to make it clear in any documentation you write, as well as any size requirements. zscript.txt and std.txt identify array arguments by adding brackets after the argument name.
Related to that, here's another pitfall. What if you want a function to create an array and return a pointer to it?
ffc script Example { void run() { int anArray = MakeArray(); Trace(anArray[0]); } int MakeArray() { int array[] = { 5, 4, 3, 2, 1 }; return array; } }
What does that Trace() print? If you guessed 5, you're wrong:
Invalid pointer (1) passed to array (don't change the values of your array pointers) -1.0000
Why? Because there's no array there anymore. If an array is declared in a function, it is deleted when that function ends, making any pointers to the array invalid. Declaring an array in a function and returning a pointer to it simply doesn't work.
You cannot compare arrays using == and !=. Like object pointers, these will tell you if the two array pointers refer to the same array, not whether the arrays are identical. If you want to compare two arrays, you'll need a function that goes over each element individually.
bool ArraysEqual(int arr1, int arr2) { int arraySize = SizeOfArray(arr1); int index; // First, make sure they're the same size if(SizeOfArray(arr2) != arraySize) { return false; } // Then compare each element for(index = 0; index < arraySize; index++) { if(arr1[index] != arr2[index]) { return false; } } return true; } // Elsewhere... if(ArraysEqual(anArray, anotherArray)) { // They match }
If you want to know the size of an array, you can use the SizeOfArray() function, as seen above - but only if it's an array of numbers. The function isn't defined for arrays of bools or pointers. In those cases, you can pass the size of an array as a separate argument to any function that needs to know it. Alternatively, you could use a constant for that purpose. While you can't use a constant to set the size when declaring an array, it's often a good idea to use a matching constant for the size everywhere else.
Global arrays are stored in saved games. The array pointers count toward the 255 global variable limit, but there is no limit on the size of the arrays. Changing the size of a global array invalidates existing saves.
One last, minor detail: the term "array pointer" is commonly used to mean two different things. It usually means a pointer to an array, but it can also mean a number that indicates the index within an array. This originates from C/C++, where the distinction isn't as clear.
Some sample scripts...
// This will create four fireballs that circle around Link. They will be // replaced whenever they disappear. import "std.zh" const int FB_RADIUS = 24; const float FB_SPEED = 2.5; const int FB_SPRITE = 17; const int FB_DAMAGE = 2; ffc script FireballBarrier { void run() { lweapon fireballs[4]; float baseAngle; float angle; // Each fireball will be recreated any time its pointer isn't valid, // so there's no reason to set them up in advance. for(baseAngle = 0; true; baseAngle = (baseAngle + FB_SPEED) % 360) { for(int index = 0; index < 4; index++) { if(!fireballs[index]->isValid()) { fireballs[index] = Screen->CreateLWeapon(LW_SCRIPT1); fireballs[index]->UseSprite(FB_SPRITE); fireballs[index]->Damage = FB_DAMAGE; } // Each fireball is offset by 90 degrees from the last one angle = baseAngle + 90 * index; fireballs[index]->X = Link->X + (FB_RADIUS * Cos(angle)); fireballs[index]->Y = Link->Y + (FB_RADIUS * Sin(angle)); } Waitframe(); } } }
// This script creates a simple snow effect. import "std.zh" const int NUM_SNOWFLAKES = 80; const int COLOR_WHITE = 1; global script Snow { void run() { int snowflakeX[80]; // X position of each snowflake int snowflakeY[80]; // Y position int snowflakeVx[80]; // X velocity int snowflakeVy[80]; // Y velocity // Randomize their positions and velocities for(int sf = 0; sf < NUM_SNOWFLAKES; sf++) { snowflakeX[sf] = Rand(256); snowflakeY[sf] = Rand(176); snowflakeVx[sf] = Randf(-0.2, 0.2); snowflakeVy[sf] = Randf(0.5, 1.0); } while(true) { // This is a very simple effect. Each snowflake moves in a straight line // and wraps around the edges of the screen. for(int sf = 0; sf < NUM_SNOWFLAKES; sf++) { snowflakeX[sf] += snowflakeVx[sf]; snowflakeY[sf] += snowflakeVy[sf]; if(snowflakeY[sf] > 175) // Fell off the bottom snowflakeY[sf] = 0; if(snowflakeX[sf] < 0) // Off the left snowflakeX[sf] = 255; else if(snowflakeX[sf] > 255) // Off the right snowflakeX[sf] = 0; Screen->PutPixel(6, snowflakeX[sf], snowflakeY[sf], COLOR_WHITE, 0, 0, 0, OP_OPAQUE); } Waitframe(); } } }
And some practice exercises.
- Create an FFC script that makes the first enemy invincible until all the others are defeated. Do this by setting all its defenses to NPCDT_BLOCK and restoring them later.Spoiler
- Find all instances of flag CF_SCRIPT1 on the screen. When every combo with that flag changes to a different one (watch Screen->ComboD[]), trigger secrets.HintSpoiler
Strings
If you want to do any sort of text processing, or if you want print text to the screen (other than with Screen->Message()) or allegro.log, you need to use strings. A string is a special int array used to represent text.
Strings are declared the same way as other int arrays. However, they're normally initialized with text in quotation marks instead of values in curly braces.
int hello[10] = "Hello"; // It's not uncommon to make string arrays a bit larger than necessary int str[] = "This is some text!"; // But it's much more common to leave out the size entirely // As with numbers in braces, this can only be done as initialization; // you can't set a string this way later. int anotherString[20]; anotherString = "Words words words"; // ERROR
Just like a regular array, you can pass the string pointer into functions. Several functions use this to load a string into an array.
int saveName[9]; Game->GetSaveName(saveName); // saveName now contains the name of the save file
You can pass the string pointer to TraceS() to log some text to allegro.log.
int logString[] = "Logging..."; TraceS(logString); TraceNL(); // Afterward, you'll find a line in allegro.log that says "Logging..."
That's all simple enough. But how about this?
int hello[5] = "Hello"; TraceS(hello); TraceNL();
If you do that, here's what you'll see in allegro.log:
Invalid index (5) to local array of size 5 Invalid index (5) to local array of size 5 Hello
Why? Because the array is too small. The string "Hello" actually requires an array of size six.
Strings in ZScript are null-terminated. That means that after all the text characters, there's a 0 marking the end of the string. This 0 is often called the null terminator. This is why Game->GetSaveName() requires an array of size 9, even though there are only 8 characters in the name.
int hello[] = "Hello"; Trace(SizeOfArray(hello)); // Traces 6 Trace(hello[5]); // Traces 0
Functions that read strings depend on the null terminator to know when they've reached the end of a string; if it's not there, they'll behave incorrectly. In most cases, they'll hang the game and log thousands of errors to allegro.log.
Because of the terminator, the length of a string and the size of the array are never the same. The length of the string is the number of characters before the terminator, which can be anywhere from 0 to one less than the array size. If there are multiple zeroes in an array, the string ends at the first one.
int abcStr[] = "ABCDEFG"; abcStr[3] = 0; // Replace D with 0, ending the string there TraceS(abcStr); TraceNL(); // allegro.log will contain the line "ABC"
Since a string is conceptually made up of text characters rather than numbers, it's worth knowing that you can signify a text character in code. Just put the character in single quotes.
int str[]="String!"; str[0] = 'a'; TraceS(str); // This will trace "atring!" // This only works with a single character. str[2] = 'bc'; // ERROR // You can use this for comparisons, too. if(str[1] == 'A') // It's case-sensitive - 'a' and 'A' are different // You can initialize a string this way, if you like. // Put the commas outside of the '' and be sure not to forget the terminator. int str2[] = { 'L', 'i', 'n', 'k', NULL }; // NULL comes from std.zh // You can even use this for regular numbers. int num = 'Z'; // That's 90
But an array really is just a string of numbers, and you're free to use it as such. There's rarely any reason to do so, however.
int hello[] = "Hello"; for(int i = 0; i < 6; i++) { Trace(hello[i]); } // Allegro.log will contain this: // 72.0000 // 101.0000 // 108.0000 // 108.0000 // 111.0000 // 0.0000 hello[0] += 2; TraceS(hello); // This will trace "Jello" // It works both ways: int str[] = { 90, 101, 108, 100, 97, 0 }; TraceS(str); // This will trace "Zelda"
These numbers are the ASCII values of the characters. All numbers in the range 32-255 can be used as text characters, but characters above 126 vary depending on the font.
Working with strings often requires the use of string.zh, which defines many functions for manipulating and interpreting strings. These functions mostly come from C, so their names look quite different than those of most ZScript functions. A couple of functions in string.zh are worth singling out here.
There are several functions for converting between numbers and strings.
// atof(): ASCII to float int numStr[] = "12.3456"; float num = atof(numStr); // num is now 12.3456
strcmp() is used to compare two strings, but it doesn't work the way you might expect.
if(strcmp(string1, string2) == 0) { // Strings match }
strcmp() returns a positive number if string1 is greater, a negative number if string2 is greater, and 0 if they're equal. What does it mean for a one string to be greater than another? Compare the two strings one character at a time. At the first character that's different, whichever string has the lesser ASCII value in that position is the lesser string. For instance, "ABC" is less than "ABZ". "ABC" is also less than "ABCD", because the first differing character is the null terminator (0) in "ABC".
Finally, the most complex functions, but also perhaps the most important, are printf() and sprintf(). These are string formatting functions. printf() writes its output to allegro.log; sprintf() stores it into another array.
// Let's say you want to write some debug information to allegro.log. // printf() is perfect for that. int format[] = "Link's position: %d, %d\nLocation: DMap %d, screen %X\n"; printf(format, Link->X, Link->Y, Game->GetCurDMap(), Game->GetCurDMapScreen()); // This will print something like: // Link's position: 80, 45 // Location: DMap 2, screen 0x2B // If you want to write the same thing to the screen, you'd use sprintf(). int line1Format[] = "Link's position: %d, %d"; int line2Format[] = "Location: DMap %d, screen %X"; int line1Buffer[128]; // Make sure your output buffer is large enough for the final string! int line2Buffer[128]; while(true) { // WARNING: There is a bug in sprintf() in 2.50.0. The output array must // be filled with 0 before using it; otherwise, it's likely to write past // the end of the buffer and hang the game. When running in a loop, // that means you have to clear out the whole thing every time. // This bug was fixed in 2.50.1. for(int i = 0; i < 128; i++) { line1Buffer[i] = 0; line2Buffer[i] = 0; } sprintf(line1Buffer, line1Format, Link->X, Link->Y); sprintf(line2Buffer, line2Format, Game->GetCurDMap(), Game->GetCurDMapScreen()); Screen->DrawString(6, 0, 0, FONT_Z1, 1, 0, TF_NORMAL, line1Buffer, OP_OPAQUE); Screen->DrawString(6, 0, 8, FONT_Z1, 1, 0, TF_NORMAL, line2Buffer, OP_OPAQUE); Waitframe(); }
printf() and sprintf() substitute special codes in their format strings with the values of the other arguments. These are listed in string.txt. In the example above, printf() sees "%d" in the format string and replaces it with the first number given, Link->X. The second %d is replaced with the second number, Link->Y, and so on. The fourth of these, "%X", prints the number in hexadecimal. "\n" means to start a new line. "\n" doesn't work in all contexts; for instance, Screen->DrawString() will not recognize it and will instead write "^".
That's it for strings. There's a lot there to remember, but you'll rarely if ever need to do anything difficult with them. Just don't forget about the null terminators and you should be fine.
As usual, some samples to end with.
// This script changes the save file name to "THIEF". item script Shoplift { void run() { int thiefStr[] = "THIEF"; Game->SetSaveName(thiefStr); } }
// This function will play the given DMap's music. It will try to play // enhanced music first; if that fails, it will play a MIDI instead. void PlayDMapMusic(int dmap) { int filename[256]; int track; bool success; Game->GetDMapMusicFilename(dmap, filename); track = Game->GetDMapMusicTrack(dmap); success = Game->PlayEnhancedMusic(filename, track); if(!success) { Game->PlayMIDI(Game->DMapMIDI[dmap]); } }
// This script will set up an FFC to run the script named AnFFCScript. item script RunScript { void run() { int scriptName[] = "AnFFCScript"; int scriptNum = Game->GetFFCScript(scriptName); ffc theFFC; if(scriptNum == -1) // No such script { Quit(); } theFFC = Screen->LoadFFC(1); theFFC->Data = 1; theFFC->Script = scriptNum; } }
And practice exercises:
- Write a script that activate level 4 cheats if the save file's name is "Zelda". Make it case-insensitive.HintSpoiler
- Make a script that displays "You got <itemname>!" on the screen whenever Link is holding up an item.HintSpoiler
Bitwise operations
Unless you plan to do some very advanced stuff, you can consider this section mostly optional. The concepts are fairly difficult, and most of these operations are rarely used. The most common use, and therefore the most important to learn, is combining and reading flags with bitwise OR and AND.
As you're probably aware, computers represent data internally as binary digits, or bits. ZScript has several operators that let you work with bits directly rather than the numbers they collectively represent. For the purposes of this section, you should think not in terms of numbers, but of bit strings - series of independent bits.
A bit of terminology, first: significance refers to a bit's position in a string and the value it represents. The leftmost bit, which represents the greatest value, is the most significant bit. The rightmost and least is the least significant bit. Bits are generally counted from 0, with the least significant bit being bit 0. A bit may be said to be set when its value is 1 and unset or clear when its value is 0.
To illustrate, here are two examples of unsigned (i.e. non-negative) 8-bit numbers.
00000001
Bit 0, the least significant bit, is set. Bits 1-7 are unset. In decimal, this number is 1.
11000000
Bits 6 and 7, the two most significant bits, are set. Bits 0-5 are unset. In decimal, this number is 192.
ZScript has six bitwise operators:
- & AND
- | OR
- ^ XOR
- ~ NOT
- << Left shift
- >> Right shift
The AND operator & takes two bit strings. The result has a 1 in each position where both of the operands have a 1.
01101001 &
11001011
--------
01001001
Be sure not to confuse the bitwise AND (&) with the logical AND (&&).
The OR operator | takes two bit strings. The result has a 1 wherever at least one of the inputs has a 1.
01101001 |
11001011
--------
11101011
Again, be careful not to confuse bitwise OR (|) and logical OR (||).
"XOR" is short for "exclusive or." A XOR B means either A or B, but not both.
The XOR operator ^ takes two bit strings. The result has a 1 in any position where one operand has a 1 and the other has a 0. Where the operands are both 1 or both 0, the result has a 0.
01101001 ^
11001011
--------
10100010
The NOT operator ~ takes only one bit string. The result has all its bits flipped from the original bit string.
~01101001 = 10010110
Note the similarity between logical and bitwise AND, OR, and NOT. What the logical versions do with bools, the bitwise versions do with individual bits.
The bit shift operators << and >> take a bit string and a number. They simply move the bits over to the left or right a certain number of places. This does not change the size of the bit string. When shifting left, the most significant bits "fall off" the end and are lost, and least significant bits become zeroes. When shifting right, the reverse happens.
01101001 << 2 = 10100100 (the two most significant bits are cut off; the two least significant bits are filled with zeroes)
01101001 >> 2 = 00011010 (the two least significant bits are cut off; the two most significant bits are filled with zeroes or ones)
In ZScript, a bit string is simply a number. Wherever the operation uses a bit string, you simply give it a number; the bit string comes from the computer's internal binary representation of that number.
A number in ZScript has 18 bits that can be used for bit operations. Unless you're quite comfortable with this stuff, however, you should only use 17. The details are too complex to get into here, but the way ZScript represents numbers internally gives rise to some oddities when the most significant bit is set. It's best just to leave that bit be.
This is a good time to point out that you can represent binary numbers directly in ZScript.
// The b at the end indicates this is binary; the number is equal to 73 in decimal. int bitString = 01001001b; // You don't have to write out all 18 bits.
Bitwise operators are used the same way as arithmetic operators.
int bits = 10010110b | 11110000b; // The result is 11110110 // You can also combine the operators with = bits &= 00111100b; // The result is 00110100 bits <<= 2; // Now it's 11010000 // Don't forget the b at the end! bits |= 1111; // That 1111 isn't given in binary; it's actually 10001010111, so the result is 10011010111
One pitfall when using bitwise operators is that their precedence is extremely low - that is, they come very late in the order of operations. This is important when using them with comparison operators.
if(x & 10b != 0) // ERROR: The compiler thinks you mean if(x & (10b != 0)) if((x & 10b) != 0) // This is the correct way to do it
ZC only uses the integer part of a number for bitwise operations.
int x = 123.456; x |= 7; // Now x is 127 - the fractional part is lost // This can be an easy way to perform integer truncation. This is not uncommon: int y = 543.21 y >>= 0; // Now y is 543
The most common use of bitwise operations is to create flags. You can effectively put several related bools into a single variable. This is a basically the same as using a number as a bool array.
// Define flags const int FLAG1 = 0001b; const int FLAG2 = 0010b; const int FLAG3 = 0100b; const int FLAG4 = 1000b; int flags = 0000b; // Set flags with OR: flags |= FLAG2; // Flags is now 0010 flags |= FLAG3; // Now it's 0110 // You'll often see them chained together. int flags2 = FLAG1 | FLAG3 | FLAG4; // To unset a flag, combine AND and NOT: flags = flags & ~FLAG3; // 0110 & 1011 = 0010 // More succinctly: flags &= ~FLAG3; // To check if a flag is set, use AND: if((flags & FLAG1) != 0) { // Flag 1 is set }
Bitwise operators are also useful if you want to use the combo solidity array Screen->ComboS[]. The numbers in that array use four bits, each representing the solidity of one fourth of the corresponding combo. Bit 0 represents the top-left quarter; bit 1, the bottom-left; bit 2, the top-right; and bit 3, the bottom-right.
0001 | 0100
-----------
0010 | 1000
// This checks whether the top half of combo 88 is at least partly solid if((Screen->ComboS[88] & 0101b) != 0) { // If either the top-left or top-right quadrant is solid, the condition is true } // This checks whether the entire top half of the combo is solid if((Screen->ComboS[88] & 0101b) == 0101b) { // Both bits have to match }
Another use for bitwise operators is combining numbers. Occasionally, you may need to keep track of two numbers, but have only one variable with which to do so. This can be done by combining the bit shift operators with OR and AND.
int x = 53; // 00110101 int y = 20; // 00010100 int combined; combined = x << 8; // 0011010100000000 combined |= y; // 0011010100010100 // These are spaced out for clarity; this would more likely be a single statement: int combined2 = (x << 8) | y; // When you want to get the original numbers back... // 0xFF is the same as 11111111b int x2 = combined >> 8; // 00110101 int y2 = combined & 0xFF; // 00010100
The downside of this technique is that it limits the range of numbers available. If you allot 8 bits to each number, as above, neither can be greater than 255.
That's the end of this section. Again, You'll probably need to understand the concept of using OR to combine flags and AND to read them at some point, but otherwise, these operations are rare.
Finally, the last set of practice exercises.
- Write a script that disables Link's collision detection if the screen flag "General Use 1 (Scripts)" is set. std.zh provides a function to check a single screen flag, but don't use it.HintSpoiler
- Write a function to set a single bit in a number to 1 or 0, returning the new value of the number afterward. SetBit(bitStr, 7, true) should set bit 7 to 1; SetBit(bitStr, 4, false) should set bit 4 to 0. Remember that bits are counted from 0.HintSpoiler
Scope and function overloading
The names of scripts, variables, and so on are formally known as identifiers. Each identifier has its own scope; that is, each has a certain region of code in which it's meaningful.
Curly braces are generally only useful to mark the bodies of scripts, functions, loops, and conditionals, but those aren't the only places they're allowed. In fact, you can stick them just about anywhere you like.
// This is all perfectly legal. Empty blocks simply do nothing. while(true) { Link->HP += 1; { Waitframe(); } { } } { } { { } }
Each set of braces defines a new scope. Variables declared within a block only exist until the closing brace.
It's not quite true that two variables can't have the same name. More precisely, two variables with the same scope cannot have the same name.
int var1; float var1; // ERROR: A variable named var1 already exists at this scope var1 = 10; { // This brace marks a new inner scope int var1; // This is fine - this makes a second var1 with a different scope var1 = 20; // The one with the innermost scope will always be used, so this sets the second var1 Trace(var1); // This will write 20 to allegro.log } // This brace ends the inner scope Trace(var1); // This will write 10, because the second var1 doesn't exist here for(int counter = 1; counter <= Screen->NumNPCs(); counter++) { npc enemy = Screen->LoadNPC(counter); Trace(enemy->HP); // Simple enough { Trace(enemy->HP); // This does the same thing as before npc enemy = Screen->LoadNPC(1); Trace(enemy->ID); // Now that a second pointer named enemy has been declared, it'll be used instead } } Trace(enemy->HP); // ERROR: Neither enemy pointer exists in this scope
You can see how this can get confusing. Be careful about reusing variable names. Declaring all your variables together at the start of a function is an easy way to avoid running into such issues.
You may recall from the section on arrays that an array created in a function no longer exists after that function ends. That's essentially the same thing: the array is deleted at the end of the scope in which it was declared. In practice, however, it works a bit differently. Its existence is limited by scope, but as long as it's still there, it can be accessed from outside its scope. When running multiple scripts together, it's possible for them to share local arrays.
int globalArrPtr; ffc script ArrayExample1 { void run() { int arr[5] = { 10, 20, 30, 40, 50 }; globalArrPtr = arr; // Give other scripts access to the local array while(!Link->PressEx1) { Waitframe(); } // When the player presses Ex1, the script will end, and the array will be deleted. } } ffc script ArrayExample2 { void run() { // If ArrayExample1 is running, globalArrPtr points to its local array. // If the script isn't running, it may or may not point to another array. Trace(globalArrPtr[0]); // This could log 10, -1 and an error, or something else entirely. } }
Logically, this should apply to functions as well, but they actually work a little differently. Although a function in the body of a script has a different scope than a global function, ZScript doesn't know how to distinguish between them. A global function and a script function can conflict.
void DoStuff() { } ffc script Demo { void run() { DoStuff(); // ERROR: The compiler doesn't know which DoStuff() to call } void DoStuff() { } }
That brings us to one final topic: function overloading. Functions are distinguished not just by their names, but also by the arguments they take. Two functions can have the same name as long as they take different arguments.
// These two functions do not conflict. The compiler can figure out which one // to use based on the arguments given. void MoveToPosition(npc ptr, int x, int y) { ptr->X = x; ptr->Y = y; } void MoveToPosition(ffc ptr, int x, int y) { ptr->X = x; ptr->Y = y; }
Function overloading isn't something you'll do often, but the MoveToPosition() function above is an example of when you might. If you want functions that do exactly the same thing but with different pointer types, that's a good way to handle it.
Only the number and types of arguments are considered; the compiler doesn't pay attention to their names.
// Putting these two functions together will cause an error. The compiler can't tell them apart. void Move(int speed, int angle) { // ... } void Move(int dx, int dy) { // ... } // Also, remember that int and float are the same thing to the compiler. // It can't tell this function apart from the other two. void Move(float dx, float dx) { // ... }
That's about all that needs to be said on the subject, so that's the end of this section.
And, in fact, it's the end of the ZScript tutorial. Congratulations on getting through it all.