PDA

View Full Version : Minor change: NPC and item pointers



DarkDragon
08-02-2007, 02:39 AM
I've changed the way item and npc pointers work: instead of expiring at the end of every frame, pointers, once loaded, now continue to be valid for as long as the underlying enemy/item remains on the screen. Even if other enemies are later added/killed the pointer should remain valid.

This change required revising a bit of the underlying ZASM, and as a consequence, any script containing screen->LoadItem or screen->LoadNPC will no longer work correctly starting in the next build. Recompiling the script in the next build will fix it.

C-Dawg
08-02-2007, 11:58 AM
So, let's say the enemy pointed to by an NPC pointer dies. Does the NPC pointer now point to the dead enemy? If you ask it for NPC->X, what will it return? What happens if you send data to it? NPC->X = 2?

DarkDragon
08-02-2007, 02:43 PM
Once the NPC dies all bets are off. ->X may do nothing and print an error message to allegro.log, or it may point to a different random enemy entirely.

EDIT: More specifically, here is how enemy pointers work:
Under the old system, an enemy pointer was simply an integer which was interpreted as an index into the screen's enemy sprite list. Thus setting an npc variable, say foo, to 2 (not possible in ZScript, but can be done in assembly), then dereferencing it, would instruct ZC to find the third enemy currently on the screen. Which enemy is the third on screen varies frame to frame depending on what enemies have been created by scripts, killed by Link, entered from the side, split from a Tribble, etc.

A while back, to fix some bug (Gleeok heads and boomerangs, I think), I added to every sprite in the game a unique sprite ID. These IDs come from a common bank and are recycled as sprites are created/destroyed, but are constant throughout the lifetime of the sprite. Under the new system, an enemy pointer is this unique ID. Dereferencing the pointer asks ZC to search all enemies in the enemy sprite list for an enemy with that ID.
If the enemy has died recently, there are two scenarios - if a new enemy has also been created which has recycled the dead enemy's ID, the pointer will happily point to this new enemy. Otherwise, ZC can't find any such enemy in the enemy sprite list and throws an error.

It would be useful to know when a pointer has gone bad; I could add a way of testing for NULL, or something like that, to ZScript, but first there is the technical problem of recycled IDs.

The problem is that without recycling, once enough sprites have been created/destroyed on a single screen to overflow the maximum representable integer, IDs will no longer be unique, leading to strange behavior and possibly crashing.

Recycling causes other problems as well, though. Create a room with a single goriya, and write a script which creates a new goriya at a random location whenever the NPC count reaches 0. Then play the quest, and kill the goriya after it throws its boomerang. The boomerang will return to the newly-created goriya; the new enemy has recycled the old's ID.

C-Dawg
08-02-2007, 06:07 PM
Why not assign IDs in such a way that the next created enemy gets the next ID in the list, and none are recycled until all possible IDs have been cycled through?

pkmnfrk
08-02-2007, 07:47 PM
Yeah, even with a 16-bit integer, you still have over 65 000 IDs to go through. And, since 32-bit addresses are cheap, bump it up, and get 4 billion IDs. More than enough for an entire quest played through several times.

DarkDragon
08-02-2007, 11:18 PM
More than enough for an entire quest played through several times.
Maybe; suppose as a worst-case we set up a screen with enough magic prisms that all 256 weapon slots are filled, and that each weapon hits a prism each frame, so that we use up 256 weapon slots per frame. We have throttling disabled so that 200 frames animate each second. Then in just under 24 hours we will have run out of IDs. Unlikely, sure, but not wholly unimaginable.

I'm going to change the IDs to use non-recycling 64-bit integers - with reluctance.

C-Dawg
08-02-2007, 11:34 PM
Well, wait, they CAN recycle, DarkDragon.

They just need to go through the whole list before they do. Get it? So after the player has assigned Enemy ID 99999999, or whatever, THEN the next enemy spawned will be 00000001. That way, you never run out, but you only get bizarre behavior once every billion years.

DarkDragon
08-03-2007, 12:04 AM
Yes, but Link and certain parts of the subscreen are sprites, and they are never destroyed, so they "hog" the first few sprite IDs. So once you wrap around, you are virtually guaranteed to have a collision.

Keeping track of which IDs are in use is not practical for 4 billion IDs.

However with 2^64 different IDs it is not practically possible to exhaust them.

C-Dawg
08-03-2007, 12:13 AM
However with 2^64 different IDs it is not practically possible to exhaust them.

Wanna bet? ;)

DarkDragon
08-03-2007, 12:53 AM
Well, there are several sprite lists in ZC - items, enemies, decorations, weapons, and perhaps a few more. Each is capped at 256 elements. Taking the number of these lists to be 10, we could burn through at most 2560 IDs per frame. This gives us 7x10^15 frames of IDs. Not enough you say?

pkmnfrk
08-03-2007, 07:37 AM
You know, at times ZC runs pretty slow with a modest amount of sprites on screen, so I sincerely doubt that we would get 200 FPS with 2560 sprites on screen:

1 frame / 2560 sprites / 200 FPS = an available 0.001953125 to draw each sprite >_>

And, that doesn't include game logic, 7 layers, etc. I predict, at best, 3 FPS.

Plus, Link has to be standing somewhere on screen. If we consider that every tile is filled with a prism, and that Link must be in one of the middle tiles (i.e., not on the edge) to fire the initial fireball, then he will die fairly quickly.

Plus, you're forgetting that it's irrelevant unless there's also scripts running which manipulate enemies.

Etc.

DarkDragon
08-03-2007, 02:02 PM
To test the enemy system I once had a script continually create Zoras. ZC handled all 250 Zoras and their fireballs without incident.

I'm not a fan of Allegro or DirectX but hardware-accelerated sprite blitting isn't all that that slow ;)

C-Dawg
08-03-2007, 05:07 PM
Alright. I see your point, and best of luck to ya. But I cringe any time we add some sort of cap to ZQuest's capabilities beyond which we know there will be a bug. That is, if in the post-2.5 world we can script additional sprite types that use up IDs, we could run into this cap. Not reasonably, but in theory.

Oh, and this also misses one of the bigger bugs we have in enemy handling. Once an enemy dies, according to this scheme, it's pointer will simply point to nothing. It won't be able to tell the script that the enemy has died. In other words, we can't have a script that takes action based on enemy death because the death of the enemy nulls the pointer.

DarkDragon
08-03-2007, 05:34 PM
Yes. Now that a dead enemy reliably has a null pointer though, I'm planning on adding something like npc->isValid().

C-Dawg
08-03-2007, 06:11 PM
Even ensuring that npc->HP returns a negative value or zero when it's null would work, too.

pkmnfrk
08-05-2007, 12:08 AM
0 would work, as a live enemy will never have 0 as an HP (since, the enemy's dead, right?)

ShadowMancer
08-25-2007, 11:03 PM
Yes. Now that a dead enemy reliably has a null pointer though, I'm planning on adding something like npc->isValid().

So what is the final word on this, I would really like to have a way to check if the enemy I am pointing to is still alive. If we use
if (enemy->HP == 0)
will it work correctly. (pointers are not recycleing now correct?)


I'm going to change the IDs to use non-recycling 64-bit integers - with reluctance.

I am just starting to write a script where this info is vital to the working of the script.

DarkDragon
08-25-2007, 11:39 PM
No, it won't. It will print a message to allegro.log warning you that you're using a stale pointer - which is not terribly useful from within the script.

I'll add these functions as soon as I finish up a fire trail bounding box bug.

C-Dawg
08-27-2007, 11:53 AM
So what is the final word on this, I would really like to have a way to check if the enemy I am pointing to is still alive. If we use
if (enemy->HP == 0)
will it work correctly. (pointers are not recycleing now correct?)


Until enemy->HP==0 returns true when pointing a dead enemy, the best we can do is tell if an enemy is ALMOST dead, then kill it ourselves and act accordingly.

Like, if you want your custom boss to change behavior when parts die, check when the part's health gets below a cut-off (like 100, a safe number because Link can never do 100 damage in one blow and short-circuit the script), and when it is, do your scripty magic and finish by killing the boss yourself (enemy->HP = 0;).

DarkDragon
08-27-2007, 01:25 PM
->isValid was added in the last build.