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 notice you have without seeing these error messages.
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, but there are two important differences. The first is obvious: the data type is different.
ffc anFFC; // An ffc pointer
The other difference is that, unlike numeric and Boolean variables, pointers cannot be global.
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 - Represents 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 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.
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 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 point to the same object afterward, but its properties may have changed.
{
void run()
{
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 any Screen->LoadXXXX function, you need to make sure the object you want is really there.
npc enemy;
if(Screen->NumNPCs() >= 3)
{
enemy = Screen->LoadNPC(3); // OK - 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.
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 Waitdraw() or Waitframe(),
// no matter what you do to it.
npc enemy;
if(Screen->NumNPCs() >= 1)
{
enemy = Screen->LoadNPC(1);
}
enemy->HP = 0;
enemy->X = 80; // This is OK; even though it has 0 HP, the enemy won't
enemy->Y = 128; // actually die until a Waitframe() is reached
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 if you know it's a concern, it may be worth checking.
// If there are a ton of eweapons onscreen already, this may fail
if(!wpn->isValid())
{
// Must be a bullet hell quest
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 it blinks out of existence. 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. So how do you do that? Your first thought might be something like this.
That seems logical, but it's actually not safe. 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.
{
if(!en->isValid())
return true;
if(en->HP <= 0)
return true;
return false;
}
// Elsewhere...
if(IsDead(enemy))
All this validation must look incredibly tedious, but it's really not all that bad. It's only needed when loading or creating objects and after Waitframe() or Waitdraw(). Even in large scripts, this is only a few checks.
In the cases where the checking is still repetitious, 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 replacement function like this.
{
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 function correctly if you don't know when your pointers are valid.
(However, in older 2.50 builds, using invalid pointers could crash the game. It should be safe now, but do you really want to risk it?)
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.
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 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 wpn2 = Screen->LoadLWeapon(2);
if(wpn1 == wpn2) // This will evaluate to false, even if the weapons are identical
// ...
wpn2 = wpn1;
if(wpn1 == wpn2) // Now it will be true
That'll do it for this section. A few sample scripts...
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);
}
}
}
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.
if(!arrow->isValid())
{
Quit();
}
arrow->X = Link->X;
arrow->Y = Link->Y;
arrow->Dir = Link->Dir;
arrow->Step = 500;
arrow->Damage=2;
// 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;
}
}
}
// and removes any enemy weapons that get close to him.
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
How might that be useful? Try loading it into slot 1 and using this as the arrow item's action script.
{
void run()
{
ffc scriptRunner = Screen->LoadFFC(1);
scriptRunner->Data = 1;
scriptRunner->Script = 1;
}
}
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.
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[200000]; // 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.
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.
// 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
Now, here's the 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.
// 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, 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 errors to track down.
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. Passing an array pointer is the same as passing a single variable.
{
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]);
}
}
}
Because the function header in the script gives no indication that an argument should be an array rather than a single variable, you need to be sure to state that, as well as any size requirements, in whatever documentation you write. It's also a good idea to make a note of it in a comment above the function header.
Related to that, here's another possible pitfall. What if you want a function to create an array and return a pointer to it?
{
void run()
{
int anArray = MakeArray();
Trace(anArray[3]);
}
int MakeArray()
{
int array[] = { 1, 2, 3, 4, 5 };
return array;
}
}
What does that Trace() print? If you guessed 4, you're wrong:
-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. Creating an array in a function and returning a pointer to it simply cannot be done.
You cannot compare arrays using == and !=. Like pointers, these will tell you if the two array pointers are pointing 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.
{
int arraySize = SizeOfArray(arr1);
int index;
if(SizeOfArray(arr2) != arraySize)
{
return false;
}
for(index = 0; index < arraySize; index++)
{
if(arr1[index] != arr2[index])
{
return false;
}
}
return true;
}
// Elsewhere...
if(ArraysEqual(anArray, anotherArray))
{
// They're equal
}
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 an 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 of an array, you can still have a constant representing an array's size.
Global arrays are stored in save data. The array pointers count toward the 255 global variable limit, but there is no limit on the size of the arrays. You should already know that modifying scripts can reassign global variables, rendering saved games invalid, but arrays take the problem even further. Unlike simple global variables, global arrays affect the size of the save data. Because of this, it used to be that modifying a quest's global arrays and reloading a saved game could crash ZC; I don't know that this has been fixed.
One last detail to be aware of: the term "array pointer" can have two different meanings. It may mean a pointer to an array, or it could mean a number that indicates the index within an array.
Some sample scripts...
// 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();
}
}
}
const int FL_ARRAY_SIZE = 16;
ffc script FollowLink
{
void run()
{
// xHistory and yHistory store Link's previous positions
int xHistory[16]; // Be sure these match FL_ARRAY_SIZE
int yHistory[16];
int linkPosIndex;
int ffcPosIndex;
// The arrays are treated as "circular buffers," meaning that the index
// will wrap around from 15 to 0. linkPosIndex is the index where Link's
// current position is stored. ffcPosIndex is one greater, the index
// where Link's position was stored the longest time ago.
linkPosIndex = 0;
ffcPosIndex = 1;
for(int index = 0; index < FL_ARRAY_SIZE; index++)
{
// It's often more convenient to set up an array in a loop
// rather than initialize it.
xHistory[index] = Link->X;
yHistory[index] = Link->Y;
}
this->X = Link->X;
this->Y = Link->Y;
while(true)
{
// Don't do anything unless Link moves. What happens if you remove this if?
if(Link->X != xHistory[linkPosIndex] || Link->Y != yHistory[linkPosIndex])
{
ffcPosIndex = (ffcPosIndex + 1) % FL_ARRAY_SIZE; // Wrap around
this->X = xHistory[ffcPosIndex];
this->Y = yHistory[ffcPosIndex];
// Increment the pointer and store Link's new position.
// This will overwrite what was previously at index ffcPosIndex.
linkPosIndex = (linkPosIndex + 1) % FL_ARRAY_SIZE;
xHistory[linkPosIndex] = Link->X;
yHistory[linkPosIndex] = Link->Y;
}
Waitframe();
}
}
}
And some practice exercises.
- Find all instances of flag CF_SCRIPT1 on the screen. When the combo at each location with that flag changes (check Screen->ComboD[]), trigger secrets.HintSpoiler
- Create an array of npc pointers. Create some enemies to store in it and make them invincible (use npc->Defense[]). Have them circle around another enemy on the screen. Kill them all when the enemy they're circling dies.HintSpoiler


