https://foon.uk/how-flash-2022/

[foon]

How I still use Flash in 2022

When Adobe killed Flash Player in 2020, I didn't want my Flash games
to disappear forever.

I've been making games on and off my whole life, but people seemed to
like the Hapland ones in particular, so I thought it might be nice to
fix them up for a Steam release. I could draw some better graphics,
improve the frame rate and resolution, and maybe add some extra
secrets and such.

[hap2c]Hapland 2

The problem is, the Hapland games are very much Flash games. The
graphics are drawn in Flash, the code was written in Flash, all the
animations are done in the Flash timeline. These games have Flash in
their bones.

How was I going to do this?

Failed attempt #1

The first thing I tried was having Flash export the games as
executables. This would have made this a very short article but it
fails because performance is about as bad as it was in 2005. I want
to make a good thing that runs at contemporary frame rates. I wanted
to be free of Flash Player.

Failed attempt #2

Secondly, I spent far too much time fiddling around with Adobe AIR, a
desktop runtime for Flash, and Starling, a library that draws your
Flash scene on the GPU.

I gave up on this in the end, partly becase AIR is buggy and
terrible, but also because I don't really want to end up with a weird
Adobe thing at the end of all this; I want to have my own thing that
I can do what I want with. What if I want to port to Linux? I don't
want to have to care about Adobe's opinion about whether I should be
able to do that or not.

The way forward was obvious; I'd have to make my own Flash player.

The plan

Here's how Hapland works. There's a tree of sprites. In Flash,
animated sprites can have code attached to certain frames, which runs
when the playhead gets there. Hapland uses this a lot. The walk paths
of the game characters are all just big long timeline animations, and
characters often have frame actions saying, when you get to the door,
open it if it's closed, or when you get to the landmine, trigger it
if it hasn't exploded yet.

[action]The little "a"s in the timeline are frame actions.

Fortunately, .fla files are just XML. I just had to parse this,
export the relevant data to a simple custom format^1 and write a
player to read it, draw the scene, handle input, and run the
animations. I'd also have to do something about the ActionScript.

Hapland would stay a Flash project, written and maintained in the
Flash editor; only Flash Player would be replaced.

Rasterizing vectors

Flash is all about vector graphics. It does support bitmaps, but it's
really designed for vectors. That's how Flash movies managed to load
so fast even on dialup connections, back in the day. All the Hapland
graphics are vectors.

GPUs don't really like drawing vector graphics. They like big batches
of textured triangles. So, I needed to rasterize these vectors.

I decided to rasterize them offline and pack the raster files into
the game. It would have been fun to have the game rasterize them at
runtime, and have it be this tiny executable, but I didn't want to
have those extra moving parts. I like having as much of my code as
possible run on my own dev machine, where I can keep an eye on it.

Flash stores its vector graphics in XML format. You might argue that
XML is a poor choice for graphics data, but you weren't a product
manager at Macromedia. Behold:

[guyxml]Vector data as seen in a .fla file

Hey, I'm not complaining, it makes my job easier.

Even though I didn't have access to a spec, rasterizing this wasn't
such a hard problem. The bezier-spline model of vector graphics is
all-pervasive, ever since PostScript. All these APIs work the same
way. After a bit of trial and error to figure out what ! and [ and so
on meant^2, I wrote a program to parse these shape definitions and
render them out to PNGs using the Mac's CoreGraphics library.

CoreGraphics was a dubious choice. I picked it because I was working
on a Mac, and it was there, and dependencies are hard. But that did
make it so I always had to rasterize the graphics on a Mac, even for
Windows builds. If I do this kind of thing again I might pick a
cross-platform library.

After rendering these PNGs, the exporter then assembles them into
atlases, quite naively. It just sorts everything by height and lays
it all out row by row like text in a document. This is far from
optimal but it's good enough.

To keep things simple, the atlases are 2048x2048 pixels, the minimum
required texture size that OpenGL 3.2 implementations have to
support.

[atlas]An atlas from Hapland 3

Rasterizing the shapes is pretty slow, so to keep build times
reasonable I needed to skip rendering things that hadn't changed. The
zipped XML format that Flash uses does have last-modified fields for
each file, but Flash doesn't seem to use them properly and so you
can't rely on them.^3

Instead, I just hash the XML of each shape and only rebuild if it's
changed. Even that fails, because Flash sometimes likes to rearrange
the XML tags in objects that haven't changed, but again, it's good
enough.

Using an assembler to write binary files

The exporter writes the animation data to a custom binary format^4.
It just goes frame by frame through the timeline and writes out all
the changes for each frame.

One thing I came up with here, which I rather like, is the idea of
writing to an assembly listing rather than straight to a binary file.
There's no CPU instructions, just data. This makes debugging easier,
because I can look through the assembly file to see what was
generated, rather than picking through bytes in a hex editor.

output.bin
                      13 92 49 EC : BD 31 E8 FF
                      09 DD BE DE : C9 5A 1D 36
                      3F C0 4E 31 : 52 FD 41 C6
                      8B 5D C0 20 : 19 1F 5F 1F
                      54 97 8C 27 : 34 1F 30 EA
                      A9 A9 E0 55 : 40 29 A3 19
                      89 BC 5F 24 : 3A 98 FD B9
                      DE 15 F2 D4 : 2A B7 41 2C
                      4E 9D 37 D9 : E2 13 4B 01
                      36 3F 40 08 : AC 3C FF 84
                      E9 AE C5 2C : 11 2F 69 CF
                      63 CE 85 D1 : A7 CB B1 1A
                      5F 5B 60 1A : 77 99 71 B0
                      60 6E C4 C7 : 73 1F EA 1F
                      31 0D 0C 39 : B0 86 70 42
output.asm
; Left Side timeline_132: ; --- Left Side, Frame 0 --- .frame_0: ;
--- Left Side, Frame 0, Layer 22 --- db Quad dd 0.152926, 0.162029,
0.184475, 1.000000 ; color dd 799.599976, -20.950001 dd 799.599976,
556.650024 dd 46.000000, 556.650024 dd 46.000000, -20.950001 ; ---
Left Side, Frame 0, Layer 21 --- ; instance of shape [Left Side]
[Wall Shadows] [Frame 0] dd Shape dw 1560
Which would you rather debug?

I could have just had the exporter write out the bytes to one file
and a separate text listing to another file at the same time, without
using the assembler, but I didn't do that because 1) assemblers
already exist, 2) I don't have to debug them and 3) they support
labels.^5

The rest of the exporter is mostly not interesting; it just walks the
tree and converts things like transformation matrices, colour
effects, and so on as it finds them. Onward, then, to the game
program itself. I chose to write this in C++ because I already know
it and new things scare me.

Scene graph

Hapland is a great fit for a scene graph^6. This is the model that
Flash used and Hapland was designed around it, so there was no sense
in trying to use a different model.

I store the scene in memory as a boring tree of Nodes, each of which
has a transform and can draw itself and accept mouse clicks. Each
game object with behaviour of its own is an instance of its own
class, deriving from Node. Object-orientation is not fashionable
right now in gamedev circles, but I'm using Flash so I clearly don't
care about that.

Flash features that Hapland used, like colour transforms and masking,
are present, although instead of implementing arbitrary masking like
Flash did, I just implemented rect clipping and edited all my
graphics so all the masks were rectangles.

Frame scripts

Almost all of the Hapland logic is in bits of ActionScript attached
to timeline frames. How was I going to export all this? I didn't want
to include an ActionScript interpreter in my game.

[fa0]A simple frame action

In the end, I settled for a bit of a hack. My exporter reads the
ActionScript from each frame and applies a bunch of regexes to
attempt to turn it into C++. For instance, crate.lid.play() might
become crate()->lid()->play();. The two languages are similar enough
syntactically that this works out fine for a lot of the simpler frame
actions, but it still left a fair bit of broken code, and there was
nothing for it but to go in and manually rewrite all the remaining
frame actions.

With all the frame scripts in C++, they are extracted at build time
and become methods on each symbol's Node subclass. A dispatch method
is also generated to call them at the right time, which looks
something like this:

void tick() override {
    switch (currentFrame) {
        case 1: _frame_1(); break;
        case 45: _frame_45(); break;
        case 200: _frame_200(); break;
    }
}

One last thing I'll call out here is that the scripting system ended
up being sort of statically typed, which was kind of neat, since
ActionScript wasn't^7. The game objects spit out by the exporter look
like this:

struct BigCrate: Node {
    BigCrateLid *lid() { return (BigCrateLid *)getChild("lid"); }
    BigCrateLabel *label() { return (BigCrateLabel *)getChild
("label"); }

    void swingOpen() { ... }
    void snapShut() { ... }
    void burnAway() { ... }
};

So, even though everything was still a sea of string name lookups
under the hood, the type-safe veneer would stop you from calling the
wrong functions on the wrong objects, saving you from that annoying
class of bug you get in dynamic languages where you typo something
and only find out about it at runtime.

Aspect ratios

Ah, aspect ratios. Everybody who converts old media to new formats
loves these. The original games were browser games that weren't even
designed to run in full screen, so they just used whatever aspect
ratio I felt like. Each game was different, but they were all around
3:2.

The most common aspect ratio today seems to be 16:9, with 16:10 also
popular on laptops. I wanted the game to look good at either of these
without any black bars or stretching. The only ways to do this are
either to cut bits off the originals, or to add bits on.

So, I drew two rectangles in each game, one proportioned at 16:9 and
the other at 16:10. The game then interpolates between them based on
the screen's aspect ratio, and uses the interpolated rectangle as the
camera view bounds. As long as all important game elements are inside
the intersection of these rectangles, and their common bounding
rectangle doesn't go off the edge of the scene, this works great.

[arbox]16:10 and 16:9 boxes for Hapland 2, against the original 3:2.

The only difficult part here was adapting the scenes themselves to
accommodate the extra width; lots of things needed redrawing and
rearranging to fit the new aspect ratios, which was a bit of a pain,
but I got it all done in the end.

Colour space nightmare

After a bit of testing, I discovered that Flash does its alpha
blending and colour transforms in perceptual space, not linear space.
This is mathematically dubious, but on the other hand I get it; a lot
of drawing programs work like this, and you want your consumer tool
to work the way people expect, even if it annoys those
head-in-the-clouds mathematicians who don't know anything about
business. But on the first hand again, it's wrong! It causes problems
with things like antialiasing.

When you rasterize vector graphics, and you ask for antialiased
output, your rasterizer will output alpha values that are so-called
"coverage values". This means that if a given pixel is half-covered
by the vector shape, that pixel will be output with alpha=0.5.

But in Flash, when something has alpha of 0.5, that means it is
perceptually halfway between the foreground and background colours.

This is not the same thing!^8

A half-coverage white pixel drawn on top of an opaque black pixel
should not be a perceptual 50% grey. That isn't how light works, and
it isn't how vector rasterization can ever work. (A rasterizer can't
say "this pixel should be perceptually X% between background and
foreground colour" without knowing the background colour.)

[greys-srgb]Blending done in perceptual (sRGB) space. Top:
Transparent whites on black; Middle: Transparent blacks on white;
Bottom: Greys [greys-line]The same blending done in linear
(physically accurate) space. Note that 50% coverage does not look the
same as 50% grey.

So, we have our antialiased rasterized shapes using one definition of
alpha, and our Flash-exported alpha transparency, gradients and
colour transforms using another. But we only have one alpha channel
in our rendering pipeline. So how should the renderer interpret alpha
values? If it interprets them as perceptual blending factors, the
semi-transparent objects will look right but the antialiased edges of
everything will look wrong. If it interprets them as coverage values,
the inverse will be true. Something will always look wrong!

There are I think only two rigorous solutions here: 1) have two alpha
channels, one for coverage and one for perceptual blending, or 2)
rasterize all shapes without AA, draw everything to a very large
framebuffer, and then scale it down with filtering.^9

I must admit, I didn't do either of these. I just accepted that
semi-transparent things look different in Flash and in the game, and
tweaked the graphics incrementally until the game looked good.
Transparent objects were never going to look exactly how I designed
them in Flash, but there aren't very many of them and it just isn't a
big problem.

To make sure I'd got everything else right, I made a "colour test"
graphic with a bunch of colours of different intensities, hue
rotation effects^10, and so on, had the game show it, and made sure
it looked the same in the game as in Flash.

[ctest]It's a match!

Frame rate

The original Flash games run at a nominal 24FPS, but really, they run
at whatever frame rate Flash Player feels like running them at. With
Flash, you might ask for 24FPS and get 15FPS, so then you'd ask for
30FPS and suddenly you'd get 24FPS. It was kind of silly.

I wanted 60FPS for the remake, which meant doing something about the
fact that the Hapland animations were authored expecting to be played
back at about 24FPS. (Flash's animation tools are based around
discrete frames, not continuous time.)

I started by having my exporter double all the frames. So, for each
timeline frame, it exports two frames^11. This gets us easily from
24FPS to 48FPS, but that still isn't quite 60, so animations were
still going to run 25% faster. The solution was good old-fashioned
elbow grease. I just played through the games and manually added in
extra frames to animations that now seemed too quick.

I now had a pretty good C++ conversion of the Hapland games that
would surely run on contemporary computers for at least another
decade or two. But I just couldn't shake the feeling that I should
try to deliver a bit of extra value, so I added a couple of things.
Aside from redrawing a lot of the old graphics and animations, I made
a couple of major changes.

Save states

This was an idea I came up with to make Hapland 3 a bit less
overwhelming. The correct path in that game is quite long, and there
are many ways to screw it up and have to start all over again. Maybe
this was fun in 2006 but we're grownups now, we haven't got time for
that.

Save states are something that emulators have. You press "save
state", and it remembers the entire state of the game by dumping the
console's memory to a file. Then, if you mess up, you press "load
state" and you're back exactly where you were to try again.^12

It wouldn't have been feasible to implement save states in the
original Flash games, because Flash doesn't give the programmer
access to its whole state. But since I'm using all my own code this
time, it is possible.

I have this thing called a Zone, which is just an allocator that
allocates all its memory inside a fixed-size chunk. All scene nodes
are allocated inside the current Zone.

To implement save and restore, I simply have two Zones, the active
zone and a separate "save state zone". To save a state, I memcpy the
active zone to the save state zone. To load a state, I memcpy back
the other way.

Second Quests

The Hapland games aren't particularly long, and although there are
three of them, I still wanted to give people a few more hours of play
time. I decided to give each game a "Second Quest"--a modified version
of the game where the layout and puzzles are slightly different.
Making such a Second Quest is less work than making a whole new game,
but still delivers some extra value.

Creating the second quests meant digging back into Flash puzzle game
development for the first time in about fifteen years, and, honestly,
I quite liked it.

The vintage Flash UI is great. Buttons have edges. Icons look like
things. Space is well-used. It's amazing! Using old UIs makes me feel
like an archaeologist discovering some sort of forgotten Roman
technology. The lost art of UI design. It's neat.

[tools]What is this wizardry?

And although Flash is buggy, slow, and missing extremely basic
features, I mostly didn't hate using it. I certainly don't know of a
contemporary program that I'd have preferred to use.

To stop the Second Quests looking too similar to the First Quests,
they get new backgrounds, and the entire scene is also flipped
horizontally.

[hap3c]Hapland 3 [hap3c2]Hapland 3 Second Quest

Music

I knocked together a quick ambient soundtrack for each game, using
stock sounds and a couple of recordings I made. Once when I was on
holiday in Japan I took a field recording at the top of a hill for no
reason in particular and it was nice to be able to use it for
something. I hired a musician from the Internet to do the title
screen music, and recorded some guitar chords myself for the end
credits, drowned in effects so you can't tell I'm bad at guitar.

I use Logic or Live for music, depending. I find Logic better for
recording, and Live better for sound design.

Achievements

I get the feeling players expect achievements in Steam games. This is
annoying because it should be up to the game designer whether
achievements are an appropriate grumble grumble, but it's not a big
deal.

Uploading achievements to Steam is a pain. You can't just define a
list and give it to their command-line tools; you have to laboriously
click through the slow, confusing miasma of PHP sadness that is the
Steam partner site and add them one by one.

I think if you're a big important game studio you don't have to stand
for that and they give you a bulk upload tool, but I'm not one of
those so I looked at the HTTP calls it was making, saved my login
cookie to a file and wrote my own.

After changing my mind several times, I opted for a modest set of
achievements: one for finishing each Hapland game, one for each
Second Quest, and a couple for the bigger secrets. Any silly, obscure
secrets that nobody will ever find do not have achievements; you just
get the satisfaction of seeing what happens.

[ach]Achievements in the Steamworks UI

Notarization

Although I developed the game mostly on my Mac, during development
Apple invented this thing called "Notarization" where if you run any
app on a new version of MacOS, it'll make a network request to Apple
to ask if the app's developer pays Apple a yearly fee. If the
developer does not pay Apple a yearly fee, MacOS will pop up a dialog
strongly implying the app is a virus and refuse to start it.

For this reason, Windows will be the first and maybe only release
platform for this game.

Libraries used

For software that I'm shipping to end-users, I like to keep
dependencies to a minimum, but I'm happy to use a few high-quality
ones. Other than OpenGL and standard operating system stuff, this is
the full list of libraries that the Hapland Trilogy executable ended
up linking to:

  * Steam SDK
  * cute_sound
  * stb_vorbis
  * stb_image

The end

And that's it! Thank you for coming on this blog post adventure with
me. It's been fun. If you do the tech stuff right, players don't
notice it at all, so sometimes you just want to say, hey, look, look
what I made.

If you'd like to read other things I've written, then you're out of
luck because I don't tend to post much writing online. If you'd like
to support me, you could buy Hapland Trilogy or my other Steam game
from 2020, Blackshift. If you want to play some of my games without
paying, I have a bunch of free browser games on my website at foon.uk
. The newest ones are Javascript or WASM, and the oldest ones,
including the original Haplands, are AS2 Flash, which runs pretty
well thanks to Ruffle. The later Flash games are AS3 so they don't
run anymore.

If you have any comments or just want to say hi you can email me at
r@foon.uk, or if you're into Twitter, I'm @FoonGames. I've got an RSS
feed too.

Take care.

Robin Allen

2022

1. It was tempting to just use SWF, the format that Flash already
knows how to export to, but this didn't work out because SWF is a
vector format and I didn't want to do rasterization at runtime, and
also because it limits your options for what to do with the scripts,
and for other reasons that I can't remember.

2. !: Move to point. | or /: Straight line to point. [ Bezier segment
to point. S: No idea.

3. That figures, too -- Flash takes an age to save large documents,
even if you only changed a single thing, so it probably doesn't track
last-modified times at all.

4. I like to use binary formats where possible for end-user software;
I don't believe in making customers' computers chew through reams of
XML.

5. It's often the case when writing binary files that you need to
write the location of one bit of data into another bit of data, and
these two locations could be in either order in the file. It's just
so easy to be able to write out dd mylabel at one end and mylabel: at
the other and let the assembler deal with it.

6. Blackshift was not.

7. ActionScript 3 started moving in that direction, but the Haplands
were ActionScript 2 games.

8. Most other image editors also work the same way. Some now have a
"do things correctly" button but it's rarely the default. If you want
a fun rabbit-hole to go down, look up how most fonts are thinner than
they should be because they're designed to be displayed by programs
that do alpha blending wrong.

9. This raises the question of what Flash does. The Flash renderer is
working with the vector data directly, without a separate
rasterization step, so it might be able to do antialiasing directly
in perceptual space as it renders, since it can check the background
colour. Or maybe it just uses full-screen supersampling.

10. Flash's colour transform effects (hue rotations, brightness
adjustments, etc.) are expressed as 5x4 matrices in sRGB space, and
it's easy enough to sort out the sRGB/Linear problem for these. For
simple matrices, you can just convert them, and for the others,
simply export the sRGB matrix and have the game compute
srgb_to_linear(ctr * linear_to_srgb(c)) in a fragment shader. It's a
few extra GPU cycles but it still all runs fine.

11. The frame doubler is smart enough to interpolate object positions
for frames that are part of motion tweens, so those stay smooth.

12. Some people had asked for an "undo button" but, say you fire a
projectile, then quickly knock it with something when it's flying,
then hit undo, what should happen? It's a case-by-case design
decision. And there are a lot of cases. With save states, I don't
have to make this judgement call.

(c) 2022 Robin Allen * foon.uk