This tutorial covers the last few major features of ZScript. These are the most difficult aspects of the language to deal with; the concepts are harder, and there are more opportunities to make mistakes. The limitations and quirks of the language will also become greater obstacles.
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
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 pointerThere is an important difference from 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 #1By 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 enemySimilarly, 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() { 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 } // It's also okay to load first and validate later lweapon lw = Screen->LoadLWeapon(10); if(!lw->isValid()) Quit(); // No such weaponIf 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 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. 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; } // Elsewhere... if(IsDead(enemy))All this validation may look incredibly tedious, but it's really not 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 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 existsWith 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 // ... wpn2 = wpn1; if(wpn1 == wpn2) // Now it will be trueThat'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; } }