Full motion video in ZZT: State of the art

It's all Xenogenesis? Always has been.


Published on November 9, 2020

In this article, we’re going to look at the history of creating animated, cinematic experiences in ZZT. Despite the engine’s harsh size limitations - 20 kilobytes per board and about 400 kilobytes per world1 creators have managed to pull off some amazing stunts, especially in the past few years of ZZT’s renaissance. Let’s go!

Warning: This article contains spoilers for a few ZZT games - The Crypt and On a Distant Moon.

Origins: The cutscene and the cinema

Where did cinematic experiences in ZZT begin? The first boards befitting the term, in my view, can be found inside Tim Sweeney’s own official ZZT worlds. Some of them, after all, do contain animated title screens and scripted events. In my opinion, the most iconic one among them can be found in Dungeons of ZZT.

The dungeon entrance, midway through its animation.

As you start the game, the path leading to freedom - the dungeon exit - is locked down, trapping you in. As the objects and sliders progress, a tune plays in the background. This sequence isn’t precisely a cutscene - you can continue playing the game while it plays around you. Later games would introduce more traditional cutscenes by trapping the player, forcing them to watch as the action unfolds.

The final board of The Crypt.

The ending sequence of The Crypt by Adam Rixey may well be the earliest example of that, having been released in July 1991 - just a few months after ZZT itself! It limits the player’s motion by putting them between a sandwich of objects moving north. In the image, they are marked with black-on-gray smileys, but they are not visible on the real board. Restricting the player’s horizontal movement has been achieved by using invisible walls made out of text. As such, it may well be the first “true” cutscene in ZZT’s history - however, as the early days of its community are not well documented, we cannot know for sure.

Over time, a certain style of creating non-interactive “cinema” would emerge. Here is what could be considered one of the earliest styles.

A cutscene board.

It seems that a kind of standard has been attempted by early ZZT pioneers of Prodigy, one of the many online services of the early 90s in the United States. In this cutscene, a pusher slowly - at a rate of slightly over two tiles a second! - moves the player from the bottom left to the bottom right, while a scripted animation plays out on the rest of the board. This technique was first used in Spring 1992 by Carlos DaSilva in the ending sequence of The Ruby of Resurrection, as well as by Chris Jong in his demo of The Big Leap: One Step Into The Future (pictured above). Interestingly, the final release of The Big Leap, as well as Jong’s later releases, did away with the pusher.

During the next few years, other creators would experiment with scripted animation in general. Season’s Greetings, a 1992 release by Chris Jong, is one of the first non-interactive, art-focused worlds, which also contains animated elements such as snow. Ned the Knight, a 1996 game by Jeremy LaMar, contains very impressive animations for its time, such as the title screen and the introductory sequence.

However, going back to cutscenes - what did the community standardize on in the end?

A cutscene board.

By the late nineties, a fairly simple, common format was adopted by many game worlds. It relied on putting the player in a corner of the board, while the exit passage was blocked by an object that disappeared when the cutscene was complete. (Pictured: The Rose by WiL.) This method was simple, effective and did not take up a lot of room. However, due to the engine’s limitations2, creating scenes consisting of many distinct areas or animations with this method requires either a ton of ingenuity or splitting the cinematic into multiple boards, connected via passages. Unfortunately, the latter option forces player intervention to progress through the cutscene’s motions. On the other hand, the existence of a blocking object made it trivial to allow the player to skip cutscenes - by explicitly touching it and moving on.

Those who are about to rock salute you.

A few game worlds dedicated solely to providing a cinematic experience would also appear. They are referred to as “cinema”-type games by the ZZT community. One such example is Freedom, a 1999 world created by kev-san and BlueMagus - ZZT’s only rock opera, as it was described. However, despite its historical notability, the experience has not aged particularly well.

Freedom, for a ZZT game, had some massive hype behind it. It featured music by Kevin Carter, aka Kev-san, who was a 13 year-old musical prodigy. It was also announced during a brief high in “Classy, deep, meaningful” ZZT games spearheaded by [BlueMagus], who would later provide the kick-ass graphics to Freedom. […] On paper, it looked like a hit.

[…] As it stands, we’re left [with] some pretty incomprehensible symbolism, athmospheric graphics with no athmosphere, music that’s up and down and then some. Yeah. […]

(Smiley19; posted on April 6, 2005)

The rationale for bringing it up here, to me, concerns it being among the worlds most affected by the need for manual intervention. Requiring the player to await for a cue, then move right and enter a passage necessarily breaks up the immersion of watching a movie (well, a musical, in this case). In addition, it can to some degree feel detrimental to the artist’s intent. However, for over a decade, this was the natural and most common method used to create cutscenes in ZZT.

Why not automate it?

Going back to DaSilva and Jong’s example - the pusher, as it moves the player in the direction of a passage, doesn’t actually transport them3. One may ask the question - is there an element which does? It turns out that yes, there is one - the Duplicator is capable of simulating player touch. For example, duplicating a gem onto a player will cause them to pick up the gem, not push them away. Likewise, duplicating a passage onto a player will cause them to transport to that board! This effect of automatically transporting the player by duplicator and passage is attributed to have been found by community member Koopo.

This means our cinematic problem is solved, right? Not quite.

Flawed duplicators

The Koopo method involves placing a player clone with an object and having the duplicator try and place the passage where the new clone has been placed. Once the duplicator activates, the passage touch causes the real player to be transported.

Unfortunately, this technique has flaws of its own:

  • The timing is not precise. At its fastest speed, a duplicator can activate once every 18 game cycles - which is about 2 real-world seconds.
  • Duplicators are noisy, meaning that something has to play in the background to hide them. This can be worked around with an object constantly playing silence, but that also removes other in-game sounds.
  • While the transition is automatic, passage transportation causes the player to enter the new board in a paused state. This means they still have to move once in order to allow the board’s elements to tick.
  • The most important flaw, however, is that transportations by duplicator are subject to the Koopo bug.

The nature of the Koopo bug was not very well understood back in the day, meaning that many people avoided this method altogether. This is how an older resource described it:

[…] [When] this method of player transport is applied, one of the objects on the next board - often one of the first ones in the list of stats - will revert to #char 255, and enter a very slow #cycle of program activity. Why this is, nobody knows […]

(zzt.org)

As such, most creators still relied on workarounds or other techniques, of which the most popular was to utilize a different, non-automatic method: placing player clones next to passages and asking the player to move in any direction. While this trick is useful for many cases - for example, object-based menus - it does not satisfy the end goal of automation.

Fortunately, the exact behaviour of the bug, as well as a proper workaround, was reverse-engineered and described by premchai21 in 2010:

It’s the tail end of the duplicator cycle.

P14 is the duplicator phase, and that gets reset to 0 at the time a duplicator performs its function; for an object, P1 is the display character, so the object displays as the blank NUL graphic. Duplicators also reset their cycle to 3(9 − P25) every cycle, and for an object P2 is normally zero6 […] so that results in a cycle of 27 […] The object that gets scrambled is the one that has the same stat index on the new board as the duplicator did in the previous board. […]

Note that this implies that non-object records, such as for monsters, can be altered as well, with different resultant effects.

So put your sacrificial stats records at the same stat index as the triggering duplicators and you should be safe, I expect.

(zzt.org forums; posted on October 22, 2010)

This detailed explanation, however, came at the tail end of the z2-centric ZZT era, after which came a long drought of very few new game worlds being created. As such, it went largely unnoticed.

Small Spaces

This was the case until, in 2019, Zephyr and Lancer-X decided to do something different.

Small Spaces is a collaborative project co-authored by many members of the modern ZZT community. While its goal was to experiment with a novel game world concept - one made out of small, resettable rooms, with most of the magic happening outside of them - the reason for bringing it up here involves two cinematics contained within it.

The title screen of Small Spaces, mid-playthrough.

The first one was the title screen, which I cannot do justice with a mere screenshot. (Therefore, if you want to get the full picture, you’ll have to watch this video of it instead.) In short, it’s a two minutes long opening sequence showing the game’s title and crediting all of the many contributors, synchronized to music. It is certainly a very impressive effort, just barely fitting in 19806 out of the available 20000 bytes and using up all 150 of the available 150 stat objects. With this animation, Zephyr pushed the boundaries of what seemed possible to do with a single board - and by approaching it in this manner, he didn’t have to worry about player transitions or the Koopo bug.

However, it is the second animation where things get more interesting.

The opening sequence of Small Spaces, as seen in KevEdit.

The optional opening sequence occupies an incredible 47 (!) boards7, all of them relatively small size-wise, portraying frames of a hand-made animation - and yet, it manages to play automatically and seamlessly before leading into the starting board. How was this done without running into the many issues and bugs standing in the way?

First of all, it avoids the passage problem by using board edges instead. Board edges are undocumented elements, used internally by the engine for the board’s invisible outer edges to transport the player to a neighboring board - hence the name. The important discovery is that they do this even if placed anywhere else on the board! In doing so, they also avoid two important problems of the passage method: they do not emit any sound as part of the transportation and they do not pause the game after completing it.

Second of all, it avoids the timing accuracy problem by pre-configuring duplicators. Duplicators have six phases, numbered from 0 to 5 - so Small Spaces just sets them8 to 5, the final phase before duplication occurs.

Third and most importantly, it avoids the Koopo bug by arranging its objects in a special way.

Player, Duplicator, sacrificial object -> Player, sacrificial object, Duplicator -> …

Note that the actual game world varies the duplicator location a bit more, but the idea is the same.

Each board pairs the Duplicator with an unused, sacrificial object on the next board in the same position on the stat list. In practice, this causes the unused object to be the only one which is modified, and - as it’s usually already invisible and codeless - this is neither noticeable to the player nor detrimental to the inner workings of the animation. Mission accomplished!

Bad Apple!! ZZT

After seeing Small Spaces’ two takes on cinematic cutscenes, a thought occured to me: Why not both? Why not combine scripted, long-duration animation with automated board transitions to create, for perhaps the first time in ZZT’s long history, a truly immersive, long-duration full motion video experience? And what better way to express my undying love for Gensokyo prove my point could there be than to bring Bad Apple!! to ZZT?

Of course, Bad Apple!! was not chosen merely due to my interest in the Touhou Project.9 Unlike most ZZT cinematics to date, which were made by hand to fit neatly within the engine’s limitations, converting an existing video would pose new problems of its own. The mostly black-and-white source material meant that I didn’t have to focus on dynamically changing colors, which would be difficult to achieve in ZZT at this scale. I could just use #char commands with a screen made out of objects arranged in a rectangle. The video consisting largely of smooth transitions also meant fewer objects would be affected in each frame - as an /i idle command takes up fewer bytes than #char, that made for a good sign as well.

First, I had to figure out the target framerate. ZZT runs in terms of cycles. At the default game speed, one cycle lasts two PIT10 ticks. The PIT runs at approximately 18.2 hertz, giving an end result of about 9.1 frames per second.

Generating the boards themselves was not particularly difficult. First, I wrote a simple tool which, for each frame of the original video, found the closest characters to represent it as white-on-black ASCII text. The algorithm for doing so was pretty naive, with pixel-by-pixel comparison and hard-coded exceptions for the dither characters, but for this type of source material it got the job done rather well. From there, I dynamically generated objects which played through a given set of frames using hardcoded /i/i/i#char... sequences within one board. Finally, I generated a set of boards connected by duplicators with the correct speed configuration based on how many frames managed to fit in the size limit of each board.

Initial experiments were based on an 18x8 (= 144 stats) window. With the duplicator and sacrificial object, this led to a total of 146 stats - very close to the limit of 150.

Prototype Bad Apple!! ZZT.

The prototype - portrayed above - kind of worked! There were two major problems with it, however:

  • The timing was completely off. Some boards ended abruptly, other ones kept the viewer waiting. Worse, the issue wasn’t consistent at all!
  • The generated world was 500KB. I set myself an arbitrary limit of 400KB, based on the world size conventional ZZT 3.2 setups were able to handle11. Now, that doesn’t sound so bad on its own - except this result was achieved at 1/3rd the framerate (!). With only 3 FPS, the in-game animation felt more like a slideshow than a moving picture.

… But then again, this couldn’t have been that easy. Let’s see what went wrong!

Timing in ZZT

Explaining the solution to the prototype’s timing issues first requires an analysis of the problem in question.

If you create a board containing, say, twelve objects moving south in a loop, each set to cycle 12, and load it in ZZT, you might notice the following two things:

  • The objects don’t move in unison; they move in sequence, creating a wave-like pattern.
  • Which of the objects moves first seems to be random and different on each world load.

What this means is that an object set to cycle 12 may run its first tick in one cycle from entering the board, in five cycles, in twelve cycles, or any value in-between. That’s not at all helpful for the goal of ensuring consistent timing. “But wait”, you might think, “can’t you just run the duplicators at cycle 1? That one should run consistently, right?”. Normally, this would be a great point - but duplicators don’t work this way.

Duplicators have six phases. After every phase, they reset their cycle count based on their speed configuration, of which they have nine: 3 cycles, 6 cycles, 9 cycles, all the way up to 27 cycles. Keeping in mind the above obseration, the amount of cycles from entering the board to player teleporation may vary quite significantly, especially at the slowest speeds (highest cycle values)! Over time, this difference between the expected and actual time of duplication adds up, throwing any hope of consistent synchronization out the window. How can this be solved?

The solution I came up with was to use an initial board, which I named the “stabilizer”. It would precede the animation proper and contain a duplicator set to run at a cycle value of N. Consider that, in the above example of “cycle 12” objects, after an object moves once - irrespective of how long that takes! - it will move again exactly 12 cycles later. Therefore, at the moment of transportation, Tick % N would equal a certain, consistent value! This, then, ensured that stats with any cycle value which is a divisor of N would run with the exact same timing every time on each board after the stabilizer.

Going back to Bad Apple!! - I chose an N of 18; keeping in mind that the speed of a duplicator must be a multiple of 3, this allowed me to have duplicators with 3, 6, 9 or 18 cycles - if Tick % N is known, so is Tick % M for any M which is a divisor of N. Combining that with the ability to control the duplicator phase, I could create frames which lasted any of 3, 6, 9, 12, 15, 18, 24, 27, 30, 36, 45, 54, 72, 90 or 108 cycles. Whew! The only additional thing I had to keep in mind was avoiding the Koopo bug - that is, never having a stat which is used in the animation occupy the same position in the stat list as the duplicator on the preceding board.

Optimizing

This left me with the world size issue - there were many tricks I used to trim it down to the desired 400KB:

  • The 18x8 window was reduced to 16x7. With only 112 objects as opposed to 144, this alone could easily save 15-20% of the world size in the best-case scenario.
  • Objects with identical code were bound to each other, which means only one copy of the code was stored in the board and in memory for multiple occurences of it. This may sound like an unlikely scenario, but given the slow transitions happened quite a few times in practice!
  • Objects with no code were turned into solids, empties, or white-on-black text depending on type. This saved quite a few kilobytes - each object occupies a minimum of 33 bytes in the board’s data - especially on boards with fewer frames.
  • In the initial image to ASCII conversion, I decided to limit which characters were used by hand. This reduced the amount of #char commands, making object code shorter. A side effect of this was making the result a bit more aesthetically pleasing, at least in my subjective view.
  • Finally, I found that just creating the largest possible frames every time was not the best way. The savings from shorter boards with fewer stats and more opportunities for identical code could easily outweight the cost of having an additional board. In addition, more flexible positioning of board splits meant that new boards could be created closer to large changes in the original video. Unfortunately, the code for this portion ended up being the kind of nightmare you can only write late at night out of frustration, after which you spend days trying to replicate it to no effect - and, in fact, only making things worse.

In the end, however, none of this was enough. To play Bad Apple!! at full speed, I would have required about two thousand frames - I only managed to generate about eight hundred frames’ worth of boards before I ran past the above-mentioned limit of 400 kilobytes. As such, the released animation plays at a mixture of 1/2 (= ~4.05FPS) and 1/4 (= ~2FPS) rates, albeit with objects tuned to cycle 2 - this way, they don’t change all at once. This trick helped hide the slower rate by making it less noticeable.

The final result

Bad Apple!! ZZT, mid-playback.

After three days of intense hacking, a slightly nicer border for the video and a hastily made #play-based cover, Bad Apple!! ZZT was released to the public to a fair amount of acclaim on January 6th, 2020. You can watch a recording of it here.

Could this result be improved? Absolutely! After the release, I spent a few more days trying out some minor tweaks and mistakes, which ended in fitting about nine hundred frames in a 400KB world. With a rewrite, more time, and some new tricks discovered since, I’m very confident one could fit the whole thing in there, at least at four frames per second. (And, maybe, I could get a better cover of the music made to accompany it, too.)

On A Distant Moon

However, Zephyr wasn’t sitting on his laurels. In the background, he was working on a new game - On A Distant Moon. One of its elements would end up knocking my socks off before it was even released.

Remember when I described the hand-crafted, animated and music-synchronized title screen in Small Spaces that ran for two whole minutes? Well, I sure wonder what Zephyr would do if he had, I don’t know, six boards to work with!

… Oh. Oh no.

Here’s a little teaser. This is the first of six boards

These are the words I woke up to in May, and it was at that point that I realized my socks disappeared into the aether, never to be found again. I ended up having to endure a long wait before receiving a follow-up on the above message, though.

The socks follow-up arrived in September - that’s when I was asked to help automating the transitions, ensuring that they’re cycle-accurate. However, Zephyr’s requests posed new, unique challenges compared to my previous project.

Slightly off is the enemy of exact

Unlike Bad Apple!! ZZT, I did not get the privilege of setting my own board lengths. What I had to work with was a set of boards with their target duration in cycles. Getting the precision down required acquiring a lot more information about the way ZZT runs, as well as rethinking some of my previous techniques. Fortunately for us, this was happening after the ZZT source code was reconstructed, so there was no more guessing involved - I could just check!

Internally, ZZT uses a global tick counter, which is incremented between 1 and 420. This means that, as long as the cycle count used in your stabilizer is a divisor of 420, the timing will remain consistent12 - a stat’s tick occurs when their ID modulo their cycle is equal to the global counter modulo the stat’s cycle.

Illustrated example.

With this information in mind, for each cycle rate which is a divisor of a stabilized cycle rate (which is a divisor of 420), you can predict on which exact cycles a given object will tick based on its position on the stat index and existing knowledge about the “cycle value” post-stabilization (Tick % N). This means causing cross-board transition after an arbitrary amount of cycles requires only a bit of clever stat placement.

Arbitrary up to a point, that is. The method used for Bad Apple!! ZZT relied on the player already being primed for the duplicator, as they were located in the top-left corner of the board, just next to it. This would put our limit at the longest possible time a duplicator can take to perform its action - 27 * 6, or 162 cycles. Unfortunately, the first board Zephyr requested was… 224 cycles, while the rest certainly didn’t get any less concerning.

If this wasn’t enough, Zephyr also asked if the player could be placed in the bottom center, so that they could be obstructed by the lyrics message. This was a rather clever idea, but it flew in the face of my approaches thus far! After all, board edge transportations relied on the player being in a corner, and the bottom center of a rectangle is certainly not a corner!

After some time and brainstorming, we came up with a solution - once again, aided by the source code. Upon a player walking into a board edge, it will first check whether or not the tile at the target location on the new board is already a player. If not, it will try to move the player to that location. The secret is in the articles - the grammatical articles, that is. After all, we have the ability to create player clones13 - they are a player, but not the player. Simply put, by putting an invisible, statless14 player clone in the exit location of the board edge, the player will not be moved on transition. This means that the player will stay where initially placed - in the bottom center! Regarding the timing problem, the answer to that was obvious in hindsight - we used an object which used #become to turn into a player clone a short while before the final cycle of the duplicator.

Of course, keeping track of all these requirements - sacrificial objects, stat list position-dependent timing, avoiding moving other stats in ways which in turn break their timing, precisely timed player-spawning objects, as well as setting the exact duplicator rates necessary - would prove incredibly annoying to do by hand! Unfortunately, as I found out the hard way, it also proved incredibly annoying to do by code. In total, a few days and many back-and-forths were spent ironing out all the edge cases.

The final result

A board from one of the teasers for On A Distant Moon.

Nuh-uh! Not this time. Play the game to see it in action. It’s really good, I promise.

Digital: A Lancer Story

Speaking of On A Distant Moon, there’s actually another multi-board animation in it - it’s easy to miss, though!

Burger Digital logo, in the form of a ZZT board.

After choosing to start the game, a short animation of the Burger Digital logo appears. However, one might notice that the fade-in is made out of two boards! This particular one is interesting in that it doesn’t use a stabilizer - and yet, the moment of transportation is always precise.

The trick here was formulated somewhat recently by Lancer-X - it relies on the concept of stat stacking. As the stat list and board elements are separate data strucutres, inside the space of one element, you can actually have multiple stats, all running in sequence. Even though they point to the same X and Y coordinates and thus the same element, they have independent stat data and thus operate separately. Since duplicators have six phases and, at the fastest speed, can run at a cycle rate of 3, this means that putting 18 duplicators in the same position will cause them to act like a duplicator which performs its action on every cycle. From there, you can just time the moment of transition with an object. The drawback of this method is, of course, that it requires eighteen stats - when you’ve only got 150 to work with in total, that’s quite a lot!

A contraption of hidden duplicators, board edges, and objects.

Zephyr, however, opted not to use stat stacking. Instead, he chose to simply build a contraption of eighteen duplicators. This is entirely understandable - this technique is a bit tricky to accomplish, while the board in question had plenty of room and stats left to use.

Closing thoughts

It is fascinating to see just how, by many small, independent discoveries and creators slowly building on top of one another over decades, the art of cutscenes in ZZT evolved from simple, in-game scripted events all the way to true full motion music videos. I hope this article will prove useful to those of you who are curious about how these techniques work. Feel free to try and replicate them in your own game worlds!

I’d like to thank all of the people mentioned above whose incremental steps formed the shoulders for other creators to build on top of. I’d also like to thank Zephyr for help with proofreading the article, as well as Dr_Dos for providing additional suggestions. I love the ZZT community to bits, and I am confident there’s still more to come from us weird fellows - so please stay tuned!


  1. The precise world size limit depends on the amount of conventional memory (typically 640 kilobytes) available after taking into account how much of it was used by DOS itself and ZZT’s binary. In practice, this leads to a limit of about 300-400 kilobytes, depending on the exact system configuration. ↩︎

  2. One of them is the board size limit of 20000 bytes, which puts a cap on how much content you can make on a single board. Even then, it’s much easier to pre-place many types of elements (STK-colored objects, et cetera) than to do so with objects. ↩︎

  3. As per the engine’s source code, pushers only have special handling for transporters, and don’t actually simulate player touch. ↩︎

  4. In ZZT, tickable elements have additional data storage known as stats. Among other things, they store the element’s cycle rate, as well as three parameters referred to as P1, P2 and P3. The official editor hides them from a creator as implementation details, exposing the specific configuration options instead. ↩︎

  5. For duplicators, P2 stores their speed, from 0 to 8. ↩︎

  6. For objects, P2 is 0 if the object in question is unlocked. If P2 is not 0, it is considered locked. ↩︎

  7. The maximum board count in ZZT is 101, including the title screen. ↩︎

  8. It also sets the cycle to 2. While duplicators do reset their cycle, it is only after they tick - therefore, if the phase is already set to 5, the duplication occurs before the cycle reset. ↩︎

  9. The demoscene also likes to use it, for reasons which are somewhat similar in principle - but different in details - to the ones which made it a good choice for ZZT. ↩︎

  10. The Programmable Interrupt Timer, a standard component in IBM-compatible PCs. ↩︎

  11. In practice, this heavily depends on your DOS configuration - how much conventional memory is occupied by it and its drivers. ↩︎

  12. Some of you might have noticed that 18, used as the stabilizer cycle count in Bad Apple!! ZZT, is not a divisor of 420. Oops. ↩︎

  13. In ZZT, the player is supposed to always be at the first stat position in a board. A player clone is a player element which goes against that assumption. Due to the way the engine is programmed, they are a bit tricky to work with, but can lead to unlocking some impressive techniques if used properly. ↩︎

  14. If a player clone has a stat, they perform all of the actions - moving, torch ticking, etc. - which a player would. If it doesn’t, it just sits there - however, this is often enough to satisfy the engine’s checks! ↩︎