Hello and welcome to the first in a series I'm calling "Ecch! Bugs! Look Out!", a series intending to take a look at the various bugs and glitches you'll come across in ZZT worlds, their causes, and how they can be exploited to benefit game development in ZZT.
For this first in the series, I thought we'd take a look at one of ZZT's more versatile glitches: the player clone.
ZZT is strictly designed to be used to create single player games, which lets it make a few convenient assumptions in its code when handling manipulating the player element. There's only ever supposed to be a singe player on each board, but creating extras is actually quite easy.
In ZZT's own editor, pressing F1 opens the Items menu, where the very first entry on the list is for placing a player with the Z key. Except this doesn't actually place a new player because again, we're only supposed to have one. Instead, this just moves our existing player to the tile underneath the cursor. This is easy enough, but there are other ways of placing elements in the editor.
Using the pattern buffer, the programmer can quickly select solid, normal, breakable, and line walls as well as empties. Tim Sweeney was thoughtful enough to not force developers from having to open a Terrain menu for each and every single wall they placed down. He was also smart enough to realize that sometimes you might want to place a lot of something that wasn't a wall, like creatures for the player to fight, or ammunition to be able to fight them.
Placing an element via the menu will cause that tile to then occupy the last position in the pattern buffer, a custom space to save on having to reopen the menu constantly. Notably however, repositioning the player does not put a player in the pattern buffer.
Luckily for us, there's another way to add things to the pattern buffer: pressing enter. This adds whatever tile is under the cursor to the pattern buffer, and just so happens to not make a check if what you're copying is a player.
So now we've got a player in the buffer, and can freely place some clones. It might be fun to place a ton of them and see what happens, but it's a lot easier to understand with just one.
So here we are with our original player on the left, and our player clone on the right. Let's play the world and experiment a little.
Before even beginning we can already see that there's something a little off. The game starts out paused which causes the original player to blink while the clone remains stable.
We take a step to unpause the game. So far so good. The clone didn't move or anything. Let's step again and...
And we seem to have become conjoined to the clone. Continuing to move around shows us constantly sticking to the clone.
Well, what about shooting? A quick cheat for some ammo...
And both the player and their clone will shoot. Note that two ammo is consumed to do this. When there's only one bullet left, shooting will make the actual player shoot and the clone do nothing (beyond making sure the "You don't have any ammo!" message gets displayed.)
Let's go back into the editor and add in a second clone. There's some weird behavior so far, but more to figure out.
This time we've got two clones, and both of them are partially obstructed by some walls. Moving around this time...
- Down jumps the player to below clone 2.
- Left jumps them to the left of clone 2.
- Right jumps them to the left of clone 1.
- And up lets the player move freely.
While we're here let's also mess around with not just ammo, but torches as well. We'll use the DARK cheat to darken the room, and then TORCHES to get some torches, and lastly we'll light one.
It might not be immediately obvious, but that torch burned out faster than normal, and unlike with ammo, only a single torch was consumed. More bizarre is what happened when the torch ran out. Only part of the board stopped being lit, and now there's just some lit up space?
Well, all of that is just an illusion. ZZT isn't updating the tiles that need to be redrawn once the torch is extinguished, and I'm not entirely sure what exactly is happening here. There is some overlap between where the clone's light would be if they could light a torch and the light from the actual player's torch, but I don't see any relation between what region is properly drawn as darkness again and what isn't.
Those still lit tiles aren't actually lit though. If anything happens to move across them causing a redraw, the darkness will properly appear once more. That's not too surprising, but it is a shame since if there was some trick to keep a lit path in a dark room via player clones it would be really useful.
With all these clones around, it's worth figuring out what happens when a clone is damaged (via shooting or explosion or anything else). We can easily make one clone shoot another if they're aligned
When a player clone is harmed... it's destroyed! No health is taken, no "Ouch!" is displayed, and not hurt sound is played. Instead the sound used is that of a creature being destroyed instead. These properties wind up being incredibly beneficial when trying to do useful things with clones as we'll see later.
There are still two more mechanics of player clones with knowing about. Firstly is a way (albeit a cumbersome one) to allow the actual player to move without getting stuck on any exposed clones. Secondly, the very unexpected behavior of what happens when you press "B" to toggle sound.
The first is pretty straightforward. Pausing and then moving to unpause does not make the player stick to any clones. You can use this to get away from them if you need to, but you won't really be making a game out of it with how annoying it can be to move a large distance while pausing each step.
Meanwhile, pressing the "B" key to toggle sound will make the player clone shoot in the last direction the player moved in (that wasn't a movement to unpause.) If you felt like you were beginning to understand how these clones worked, that one may catch you off guard. It's definitely some peculiar behavior.
I think that about covers what's worth observing with player clones to get a feel for their behavior. Next let's take a look at the technical explanations for this behavior.
Firstly, is the stat order. Elements that have stats are ones that can be customized in any way beyond just color, as well as a few others. Lions have a stat so they can have an intelligence value. Passages have stats to store their destination. Keys just sit there and do nothing, so no stat. The player handles keyboard input and has stats. Objects have code and character and a lock state and very much have stats.
There's a hard limit in ZZT of 151 elements with stats per board. Every board has to have a player, so you really get 150 of your own choosing. Stats are how ZZT processes elements each cycle, it simply runs down the list of stats, sees what element is at the stat's coordinates, and executes whatever it needs to do. Normal behavior keeps the two tied together, as when an object moves, the stat's coordinates update as well, but some other bugs to be covered in later parts of this series will explore situations where that might not be the case.
If we place something like this: a player, a lion, a bomb, and an object, ZZT will update those elements in that order as well. Player clones abide by the same rules as the actual player, but don't seem to. The odd behavior originates from the assumption that every board has a player. Since every board has to have a player before it has anything else, the real player will always be the first stat on the board, and thus the first to execute. Sweeney took advantage of this to reduce crawls through the entire stat list when dealing with a player. For example, if a lion's stat updates and it decides to move towards the player rather than randomly, it needs to get the coordinates of the player. Instead of going through the whole list and finding the stat whose coordinates point to a player on the board, it's a safe assumption to bypass all that and just use the first stat which is going to be the true player.
The assumption that the first stat is the player and there is only one player is the cause of our sticky clone behavior. Let's look at our pair of clones again, but this time put a gem next to all three players. Now we press up...
...and all the gems are gone!
ZZT is going in stat order so the real player moves first, and collects the gem. The clone then acts and sees the keyboard input as well. It also wants to move north, but it's assuming that it too is the first stat, and instead moves the first stat north from its coordinates! The real player is warped to the clone and collects the next gem, but we'll never see that because the final clone then gets its own turn where it too sees the input and moves the first stat north as well! The end result is that the player gets to occupy multiple tiles in a single game cycle, collecting all the gems simultaneously!
This also explains the clone the player sticking to changing when walls are around. If the clone is blocked, there's no need for it to try and move the player, so the stat remains unchanged, letting the player stop in the middle of a chain of clones if all the others after a certain point are blocked in a direction.
Shooting uses the same behavior, each player takes 1 ammo and produces one bullet if it can. And the short torches? The first player lights their torch, the clones meanwhile see that they're supposed to light a torch, but one is already lit and do nothing to the torch count. They do however execute the code to decrease the time remaining on the torch, get a lot of clones in place and torches can extinguish almost instantaneously. The same thing happens as well when the player collects an energizer and clones present result in a decrease in the time the true player is invulnerable.
So what about the torch residue? Well... I don't actually know? It seems really inconsistent what tiles get redrawn as dark or not based on the true player's torch radius and if it overlaps with what would be the torch radius of any clones. As the glitch is entirely visual and fixes itself whenever something updates, there's not much you can do with it to begin with.
The clone destruction when shot? Default behavior. The bullet has its own stat and sees that it hit something. That something isn't the first stat element, so the ZZT assumes that whatever the bullet just hit wasn't _the_ player so there's no need to run the damage function. Either ZZT defaults to assuming the shot element must be a creature instead and runs code for that, or the player clone still handles code on its own for its own destruction followed by replacement on the first stat's coordinates.
Pressing "B" making clones shoot is a bit more interesting. If you take a look at the (partially) commentated disassembly of ZZT by Kev Vance, you can see where this behavior comes from.
TestB: ; CODE XREF: TickPlayer+49E↑j
cmp al, 42h ; 'B'
jnz short TestH
cmp SoundEnabled, 0
jz short EnableSound
mov al, 0
jmp short UpdateSoundState
; ---------------------------------------------------------------------------
EnableSound: ; CODE XREF: TickPlayer+4B7↑j
mov al, 1
UpdateSoundState: ; CODE XREF: TickPlayer+4BB↑j
mov SoundEnabled, al
call StopPlayingSound
call UpdateSideBar
mov LastKeyCode, 20h ; ' '
jmp short CheckTorchLit
For those who don't speak assembly (myself included really), the player checks if the "B" key has been pressed, and if so will reach this UpdateSoundState label. sets the sound value to 0 or 1 based on the value of SoundEnabled. Towards the end of all this, the LastKeyCode value is set to "20h", which like the comment shows, is a space character, and coincidentally the one which is also used for shooting in the last direction. So unlike with moving or shooting normally, when "B" is pressed, the true player acts like "B" is pressed, but all the clones instead see space as the last key.
I'm not entirely sure why the code updates that button, as none of the other player controls mess with LastKeyCode to fake a key. One thing I discovered in the process of writing this is that the ability to shoot in the last direction moved with the space key only exists in ZZT version 3.2 and above. So my theory is that it's in fact meant to handle player clones as otherwise a game could force audio state by having an even number of players on the board since one player would turn the sound off, and then the next would turn it right back on. Since being able to produce clones is likely unintended to begin with, it would be necessary as a failsafe in case somebody made a board with clones in an older version had 3.2 prevented their creation to begin with.
Who knows really!
There is of course more behavior to player clones to explore but this should serve as a nice introduction. Lastly, let's look at a few practical applications for all this theory.
1. Teleporters
By taking advantage of the sticky property of clones, and the ability to
create them in ZZT-OOP with a simple #put <dir>
player
, it's easy to create a teleporter which will force the true
player to an arbitrary spot on the board. In the video above, a dark purple
object at the top of the teleporter checks that the player is adjacent to it,
and begins the process of teleporting. It sends a message to the destination
teleporter to place a player. Due to the surrounding walls, the player can only
move to the south, which will cause them to stick to the clone. A third object
in the lower left corner of the destination teleporter.
2. Long Distance Teleporters
Not quite the same thing, but a very similar technique can be used to transport the player not just to somewhere on the current board, but to a new one entirely!
The simplest way to do this is to just surround an object with passages and have that object turn itself into a player clone. This requires the player to take a step before the warp happens, but is pretty straight forward to code. A more popular technique is to instead have a duplicator duplicate a passage directly on top of a player clone. This allows the warp to happen automatically (though it's not without its own bugs that are worth an article of their own quite frankly) and is much more smooth looking.
Taking the player to a new board like this to jump to a cutscene or battle is probably the most common use of clones in ZZT.
3. Capturing Input
Many engines in ZZT work by surrounding the player with objects that can be touched and then send messages to another object. Shmups, racing games, and platformers most of all typically require the player be surrounded on at least three sides if not all four. Surrounding the actual player in all directions is a little dangerous, since the only way to unpause is by moving, and the game starts paused when restoring a save file. This is often mitigated by instead having a gap and relying on the surrounding objects to instead check that the player is in contact with them before pushing the player back to the center, and also has the added benefit of letting the player shoot the objects they're surrounded by as well.
You can substitute these for a player clone surrounded with arrows. For shooting: surrounding them with breakables, and then having objects check when a breakable is no longer blocking them and replacing it (so as to not expose a clone and have the real player get stuck next cycle). This does take up a bit of space unfortunately. On the bright side, if all you need is to capture that the player attempted to shoot and not the direction, you can use this more compact layout.
Every time I play an adventure game that requires the player to open the cheat prompt and type +I, and I complain that the setup should just use the space bar, this is what I'm talking about! If you don't have shooting in your game, the space bar is a free easy to access source of input for whatever else you like!
4. Crowds
This one isn't quite the same as a regular player clone, as it instead relies on player clones that have their stats removed. Unfortunately you can't produce these in ZZT via the editor or ZZT-OOP. They need to be obtained from a toolkit or created with an external editor. We've seen how stats are used to process what tiles need to take an action each cycle, but it turns out even without stats a player clone has its uses!
When making crowds of people, you'd typically use objects with smiley face characters. The problem is, you have to worry about the stat limit if you're really trying to fill a room. If you remove the stats from an object, it will default to the blank character 0, but players are always character 2. By using statless player clones, affectionately referred to as "dummies" or "dead players", you can get a statless smiley face and fill the board to your heart's content. These dummies can be any color combination as well, whereas clones with stats are forced to white on dark blue due to players' colors being reset due to the code that handles the flashing colors and character of a player when they pick up an energizer.
In the above example, there are only eight stats used on the board, and they're all in the stage area. The entire audience section uses zero.
Player clones are surprisingly versatile really! For something that's not even supposed to exist, there's quite a lot you can do with them, and each of the listed use cases helps make up for some of ZZT's notable deficiencies. The ability to force a board transition, arbitrarily reposition the player, read input without restricting the player, and just make some decorative elements without worry are all quite useful compared to some of the more bland bugs we'll see in the future. It's quite impressive how much the capabilities of ZZT expand once you have an understanding of clones!