Status update: 2026

Hey everyone! To reiterate my opening sentence from last time: It’s been a while since I last posted, but don’t worry, I’ve been keeping busy.

Normally, I’d dive into the technical details, but not this time. It’s been too long, there’d be too many of them, and even I don’t remember them all. I’d have to read through and summarize ~1200 commits, and if I wanted to spend my time doing that kind of thing, I’d get a job as an LLM.

Instead, how about we skip to the part everyone cares about? The Run 1 and Run 2 remakes are both close to completion, with all levels (plus Infinite Mode in Run 1) working. However, both games still have a list of issues.

Run 3 remains on the back burner until those first two are ready.

Run 1

I’ve already released an HTML5 remake of Run 1 (which I consider to have been a beta version), but since then I rewrote most of Runaway (the underlying engine), so I’ve had to redo most of Run 1 as well. It’s coming along well so far, and since I invested in building solid foundations, fixing the remaining problems should be relatively painless.

Adventure Mode is nearly done. The levels are unchanged, but I’ve been playing around with the physics, trying to make the game feel nicer but still close enough to the original. I’ve also done my best not to mess up game balance. I know people like Run 3’s physics, but those would make Run 1 too easy.
Side-by-side comparison of Level 1, near the end of the level. The images are similar, but the old one looks blockier and the Runner looks smaller, while the new one looks smoother and the Runner is closer to the center of the screen.

There’s still more to do for Adventure Mode. For one thing, it saves your progress but can’t load it, so you always restart from level 1. Also, there’s no ending, and the level transitions are missing something. At the moment I’m still using the “flying through space” level transition from the beta, but it’s less interesting without all the tiles zooming into place. Either I need to recreate that animation, add other scenery to look at, or go back to the original “endless tunnel” version.
Side-by-side comparison of a level transition. The old version features an endless tunnel with a text overlay instructing the player to "Press space to continue." The new version features the Runner flying through space, approaching the next level.

Infinite Mode is finally working! This one took a while.
Side-by-side comparison of Infinite Mode at 45% difficulty. The new version looks harder, but that's partially because the Runner happens to be starting on the wrong side of the tunnel.

I tried to adapt the original level generation code, but something went wrong along the way. The new generator makes levels that are both harder and less visually coherent. As for why it’s green, I haven’t gotten around to adding multiple colors yet.
Side-by-side comparison of Infinite Mode at 99% difficulty. The old version has several clumps of tiles and at least two viable routes, whereas the new version has tiles in groups of 1-3, and fewer tiles overall. There are no good routes in sight, even if it's technically possible.

In previous status updates, I talked about how Run 1’s Infinite Mode required an overhaul of how levels were loaded. At the time, my plan was to place the levels in 3D space, loading them as soon as two conditions were met: (1) the camera was almost close enough to see them, and (2) the player beat the previous level. The original version of Run 1 would only load a new level once the previous one was over, and it’d use the player’s performance to determine the difficulty of the next level. The more you fell, the less difficulty would increase (though it would always go up by a little).

Well, I did that. And then I did something else instead. Whoops!

Now, Infinite Mode levels load based on the camera’s position (1), without waiting for you to beat the previous level (2). The difficulty increases as you make progress, and the tiles update in real time, which I think makes for a cool effect. (Also, the difficulty decreases each time you fall.)

I like this concept, and it took a fair bit of work to make it happen, but I’m not sure I like how it feels in practice. Pros: if it generates an unfair level, things will get easier once you fall off a couple times. Cons: it feels less like you’re making progress if you can just lose it again. In the original version, any progress you made was locked in. I could go back to that, but I think (hope) this should be fixable while keeping the cool new real-time updates.

Finally for Run 1, there’s currently no Edit Mode or costumes. I’ve got a working editor in Run 2, and I should be able to import that without too much trouble. Not sure what I want to do about the costumes, though. Do I keep them as costumes, or is it worth the time to flesh them out into separate characters with unique abilities?

Run 2

For those who weren’t there, in late 2024 I rebuilt Run 2 in a series of livestreams, better than before. New UI icons, touched up levels, clearer indicators of where you can and can’t rotate gravity, higher-resolution backgrounds, and a new sci-fi font.
The Skater at the start of "Jigsaw Pieces." To his left is a gap in the floor; the wall of this gap is marked with diagonal slashes, indicating that if he can't catch on to that wall. The pause menu is open, showing a series of icons: a house, left and right arrows, a yellow circle, and a music note. These icons are drawn in one continuous stroke, except the note, which uses two strokes.
I’m sorry it’s taken so long to get this thing out. Many of us (me included) expected it to be out by now, but some of the remaining problems proved stubborn. At this point, I’m going to wait until I’m ready to release both remakes at once.

Here’s what was broken as of my last stream.

  • The player character stutters, as if updating at a low framerate. The rest of the game moves smoothly, so it’s not actually a performance issue. And Run 1, which is using the same engine, doesn’t have this issue. Still don’t know what’s up with this.
  • Sometimes when you deleted a cube in Edit Mode, it wouldn’t disappear, but you could fall through it. I’m happy to report that this is now fixed!
  • Sometimes when you added a new cube, nothing would show up but it would still be solid. Also fixed!
  • Often when you clicked and dragged to “paint” several cubes at once, it would place some in the wrong layer (usually one layer closer to the camera than expected). Fixed!
  • There’s no way to edit level properties (color, dimensions, title, etc.) other than manually editing the level data. I still haven’t done anything about this.
  • Though you could save levels, there was no way to load them. I’ve since added a basic text box to paste level data into, and levels do load successfully, but this promptly revealed a bunch more issues.
    • The UI isn’t built to deal with standalone levels like this, so the next/previous/bonus buttons do nothing and display incorrect data. They always indicate that there’s a previous level and that you haven’t beaten the current level or earned its bonus. When you do beat the level it says “press jump to continue,” but you just jump normally since there’s no next level to go to.
    • I didn’t program the levels to unload, so they just stay around until you quit to the main menu. This lets you load multiple levels on top of one another, though you can only interact with the most recent one.

In addition to all that, a new rendering issue popped up. If you leave a level and then navigate back to it using the pause menu, only the level’s outlines will appear. I distinctly remember having fixed this at one point, but no, it’s still here.
The Skater at the start of "3-Tuples." The cubes in the level are invisible, but the outlines are there.
This looks pretty neat in some cases, so I might add official support for it later. But I can’t keep the bug itself, as some levels lack outlines, and with neither fills nor outlines they end up being fully invisible. Good luck playing those!

As if all that wasn’t enough, I recently tried building for HTML5. It starts out in outline mode, and then the scene goes completely black if you ever move left or right. You can’t complete (or fail) the level in this state, but everything comes back if you reset, so it’s not like the game crashed. I suspect the Runner’s coordinates are getting divided by zero.
Two invisible levels. The first shows the Skater skating on nothing in "There Has to be an Easier Way." There are still background stars in that one. The second is the Runner playing "Left, Right, Space," but everything is gone: the Runner, the level, and even the background stars. Only the title bar remains.

What next?

I plan to fix everything mentioned above. When these games come out, I want you to be able to do everything you could do in the original. I’ll try to leave it at that, and not get distracted working on new features. Though it occurs to me as I write this that an “import save” feature would help a lot. Then you can pick up exactly where you left off.

I also want to do some marketing, to build interest. Keep an eye on this blog and my YouTube channel for updates.

Status update: summer 2022-winter 2023

It’s been a while since I last posted, but don’t worry, I’ve been keeping busy.

Last time, I talked about spending the first half of 2022 working on Lime. Since then, most of my changes were categorized and merged. Small bug fixes are to be released in v8.0.1, while everything that counts as a new feature will come out later in v8.1.0. Except for the two huge changes that I spent 4+ months on; those have to wait for v8.2.0 because we want more time to test them.

I spent most of the second half of 2022 on my own projects, gradually working my way up through the layers Run is built on.

Echoes

Starting in late June, I turned my focus to Echoes, which I’ve previously described as my favorite entity-component-system framework available in Haxe.

Over the summer, I rewrote and documented practically the whole codebase. I fixed bugs, cleaned up the unit tests, added new features like better timekeeping, wrote lots of documentation, and generally improved maintainability. But I never lost sight of my goal, which was to get back to work on Runaway. I only added features that I knew I’d need immediately, rather than trying to plan ahead too far.

Runaway

Between August and October, I split my time between Echoes and Runaway, gradually shifting towards the latter but going back to Echoes whenever I found a new bug or needed a new feature. And the first part of Runaway I rewrote was the math. Math is fundamental to games, and it’s worth doing right.

Take this code from my “What is Runaway?” post:

class MotionSystem extends System {
    @:update private function accelerate(acceleration:Acceleration, velocity:Velocity, time:Float):Void {
        velocity.x += acceleration.x * time;
        velocity.y += acceleration.y * time;
        velocity.z += acceleration.z * time;
    }
    
    @:update private function move(velocity:Velocity, position:Position, time:Float):Void {
        position.x += velocity.x * time;
        position.y += velocity.y * time;
        position.z += velocity.z * time;
    }
}

Here, MotionSystem performs two basic operations. First it uses Acceleration to update Velocity, then it uses Velocity to update Position. Because this code gets run every frame, it produces the appearance of 3D motion.

To keep things simple for that post, I handled x, y, and z on separate lines. However, you really shouldn’t have to do that. Any decent math library will provide a more concise option. Before August, that was this:

class MotionSystem extends System {
    @:update private function accelerate(acceleration:Acceleration, velocity:Velocity, time:Float):Void {
        Point3Utils.addProductOf(velocity, acceleration, time, velocity);
    }
    
    @:update private function move(velocity:Velocity, position:Position, time:Float):Void {
        Point3Utils.addProductOf(position, velocity, time, position);
    }
}

Convenient! I know typing out Point3Utils.addProductOf() every time could get annoying, but it still beats OpenFL’s Vector3D class:

var scaledAcceleration:Vector3D = acceleration.clone();
scaledAcceleration.scaleBy(time);
velocity.incrementBy(scaledAcceleration);

Not only is Vector3D less convenient, you have to make a temporary object. For one object that’s fine, but video games can do hundreds if not thousands of these operations per second, and all of those temporary objects have to be garbage collected.

It’s at least possible to make Vector3D more convenient. Just add a “scaledBy()” function that calls clone() and then scaleBy(), combining lines 1 and 2 above. It still has the garbage collection problem, but at least the code looks neater:

velocity.incrementBy(acceleration.scaledBy(time));

But wait! It turns out you can avoid the garbage collection problem too, using inline constructors.

If we inline Vector3D’s constructor and functions, then the Haxe compiler will copy all that code into our function call. After moving the code, the compiler will be able tell that the clone is temporary, and doesn’t actually need to be allocated. Instead, the compiler can allocate local x, y, and z variables, which incur no garbage collection at all. Sadly, pretty much none of Vector3D is inlined (either for backwards compatibility reasons or because this isn’t a priority for OpenFL).

So I looked elsewhere. As luck would have it, the hxmath library provides a fully-inline Vector3 type. Not only does this solve the garbage collection issue, hxmath’s Vector3 offers a lot more features than OpenFL’s Vector3D. (IMO, hxmath is the best math library on Haxelib. Not that there’s much competition.) In particular, it allows you to use operators wherever such operators make sense:

class MotionSystem extends System {
    @:update private function accelerate(acceleration:Acceleration, velocity:Velocity, time:Float):Void {
        velocity += acceleration * time;
    }
    
    @:update private function move(velocity:Velocity, position:Position, time:Float):Void {
        position += velocity * time;
    }
}

(As of this writing, the += operator isn’t available on Haxelib. But it should come out in the next release.)

I initially planned to join forces, submitting improvements to hxmath rather than creating yet another math library. I submitted fixes for the bugs I found, but as I looked closer I started to find more and more things I’d do differently, as well as a couple inconsistencies. Anyone already using hxmath wouldn’t like sudden changes to function names or the order of a matrix’s elements. Breaking changes are always a hard sell, and I’d need to make a lot of them before I could use hxmath for Runaway.

So I began working on my own math library (working name “RunawayMath”), using the knowledge of macros I gained from working on Echoes. Specifically, I wrote macros to handle the repetitive parts. For instance, my Vector3 class can do in 13 lines what hxmath does in ~55.

@:elementWiseOp(A + B) @:inPlace(add)
private inline function sum(other:Vector3):Vector3;

@:elementWiseOp(A - B) @:inPlace(subtract)
private inline function difference(other:Vector3):Vector3;

@:elementWiseOp(A * B) @:commutative @:inPlace(multiply)
private inline function product(other:Vector3):Vector3;

@:elementWiseOp(A * B) @:commutative @:inPlace(multiplyF)
private inline function productF(scalar:Float):Vector3;

@:elementWiseOp(A / B) @:inPlace(divide)
private inline function quotient(other:Vector3):Vector3;

@:elementWiseOp(A / B) @:inPlace(divideF)
private inline function quotientF(scalar:Float):Vector3;

@:elementWiseOp(-A) private inline function inverse():Vector3;

@:elementWiseOp(A + B) tells the macro that when you type vectorA + vectorB, it should make a new vector with the sum of their elements (vectorA.x + vectorB.x, vectorA.y + vectorB.y, and vectorA.z + vectorB.z). @:inPlace tells the macro to enable the corresponding assign operator (+= and so on).

With macros in place, I was able to quickly churn out data types. First I re-implemented most of hxmath’s types, like vectors, lines, and quaternions (but skipped matrices because I don’t need them yet). Then, I added brand new ones, including spheres, planes, orthonormal bases, bivectors, and Bézier curves. At some nebulous point in the future, I plan to implement matrices and rotors.

I made sure to write unit tests as I went, something I’d never bothered with before. As a result, I’m confident that these new types will be less buggy than my old code. Notably, Run 1’s beta lacks shadows due to some sort of error in either my raycast code or my quaternion code. I never managed to find the issue, but I bet my new code will fix it without even trying.

I could go on about all the new features, but I really should save that for some other post.

Practical applications

By November, I was ready to start making use of all this mathematical awesomeness, by returning to the fundamentals of any platformer: running and jumping.

Well, running specifically. It’s the name of the game, after all!

Side-to-side movement

It isn’t really a secret that Run 1’s HTML5 beta looks and feels different from the Flash version, and part of that is the way the Runner moves side-to-side.

The old formula for acceleration (in Flash) used the slow Math.pow() function to produce a specific feeling. When I used a totally different function in HTML5, it produced a different feeling. I did that because I figured that if Run 3 didn’t need Math.pow(), Run 1 didn’t either. Problem was, I wasn’t very careful writing the new formula.

Now back to November 2022. This time, I wanted to try a suggestion I read in an article or social media comment. The idea is to graph speed vs. time, letting you see (and edit) how fast the player character accelerates.

The simplest case is constant acceleration. You gain the same speed every frame (until you reach the maximum), producing a straight line on the graph.

But that’s restrictive, so what about curved acceleration? Specifically, how about using those Bézier curves I mentioned above? It’s easy to get acceleration from a curve that represents velocity, and it lets me play around with a lot of options to see how they all feel.

And that’s without even changing the top speed or acceleration duration. I meant it when I said a lot of options. (Click to view.)

Out of all those options, I think the ones with high initial acceleration (convex shapes) feel the most natural.

The initial burst makes the controls feel more responsive, and then they gently approach the top speed so there’s no sudden jerk. Not only that, it’s true to life: as someone picks up speed in real life, air resistance gets stronger and they accelerate slower.

That said, the curves I showed above are extreme examples. Usually I aim for a bit less acceleration at first and leave a bit of jerk at the end. That way, it’s more obvious that you’re continuing to gain speed over time.

I don’t know which of these most closely matches the original Run 1, but I plan to find out.

Other basic components

By the way, my colorful-square demo app is more than just a rolling simulator. It’s actually a small platformer demo, with collisions and jumping and all that. Just… without any actual platforms, yet.

The “Mobility” button brings up the acceleration graph I’ve already shown. The “Jump” button lets you control how high each type of jump goes (or to disable specific types such as midair jumps). “Air” lets you tweak air resistance and wind speed.

But that’s far from all the components that went into this app. I also took the chance to rewrite and test each of the following:

  • Collisions, both for the invisible walls around the edges and for the boost pads in the middle. Solid collisions are automatically classified as “ground,” “wall,” or “ceiling,” making it easy to write ground-specific or wall-specific behavior. Like the walljump seen above.
  • Friction, including friction with moving platforms. A moving platform will pull you along with it (unless it’s icy), and you’ll keep that momentum if you jump. (I expect to use this for conveyors in Run 3.)
  • A brand new Orientation class that replaces the coordinate space approach I used to use. Turns out I was making things unnecessarily hard for myself, rotating the character’s coordinates rather than rotating the effects of the left/right/jump buttons.

How long can player_03 keep stalling?

As of this moment, I’m running dangerously low on things to do before I get back to work on Run 1. I’ve been putting that off working hard on underlying projects since… summer 2021? It’s certainly taken a while, but soon I’ll be done stalling preparing.

  • When Infinite Mode didn’t work, I could have looked for an easy workaround. Instead, I rewrote all loading code.
  • When I realized loading is best done using threads, I could have just used the tools that existed. Instead, I spent several months revising Lime’s threading classes and adding HTML5 support.
  • I made a decent-size demo project just to show off the new thread code. That took at least a month on its own, and I have no immediate plans for it, but at least it made pretty patterns.
  • I spent months cleaning up Lime’s submodule projects instead of leaving well enough alone. Now, I know more about this section of the code than almost anyone else, so I’m the one responsible for any new bugs that come up. Fortunately there haven’t been too many.
  • I spent over a month (hard to measure because it’s spread out) updating Echoes, even though it demonstrably already worked. It had a couple hiccups, sure, but I could have fixed those without rewriting all the documentation and unit tests. But hey, now we have more documentation and unit tests!
  • When I discovered constructor inlining, I dropped everything to rewrite Runaway’s math classes. They already worked fine, but nooo, the code was a little bit verbose, we couldn’t have that!
  • With new math classes available and a vague sense that I was overcomplicating space, I stumbled across the term orthonormal basis. And that inspired me to rethink the concepts of space, collisions, moving, and jumping all at once. I’m going to need to write a whole new “how space works in Run” post…
  • To take advantage of all this new code, I wrote the demo app shown above. It took about as long as the libnoise demo, but at least this time I plan to keep using it.
  • When first making the demo, I enabled one system at a time, so that I’d only have to debug one section of code at a time. Eventually, I decided just to enable all the systems from the Run 1 beta, even knowing I’d immediately get dozens if not hundreds of errors. I figured it’d let me keep stalling for a while longer locate lots of bugs all at once.

I estimate that fixing that last batch of bugs will take at least… oh. Uh, they’re already done. Took less than a week. I guess there was a benefit to all that feature creep careful planning and solid mathematical foundations.

Except, that is, for one single system. This system was especially complicated, and since I wanted to post this post, I left it commented out. Once I get this system working, I think that will be everything: I’ll be unable to keep stalling ready to work on Run 1.

I’ll be ready to polish the wonky character animations, make the motion feel more like it used to, and, of course, update the level load system to handle Infinite Mode levels, all those things I’ve been saying I want to do. But first, I need to fix up this one complicated system.

So which system is it? It is, in fact…

…the level loading system. Also, the only reason it’s so broken is because I made those other changes to how loading works. Fixing the errors will automatically also make the system compatible with Infinite Mode.

Huh. I guess that means I’m already done stalling. No more redoing old features, I’m officially now working on the thing that started this entire multi-year rabbit hole. Or maybe I could go implement rotors first… One more detour…

What is Runaway?

I’ve been working on Runaway since (at least) 2019, and talking about it in vague terms for about that long. But what is Runaway, really?

Before getting started, let’s clear up some terminology. Runaway is a game engine, named after Run (the game series) and Away3D (the 3D rendering library). The Runway is a tunnel in Run 3 that hasn’t been released. Despite the similar names, there’s no connection.

Overview of Runaway

A game engine is a collection of code designed to help people write games. You may have heard of engines such as Unity, Unreal, or GameMaker. Haxe – the language I named this blog after – has engines such as HaxeFlixel, HaxePunk, and/or Armory. All of these are designed to serve as a solid foundation for making games, saving you the time of re-writing your physics and rendering code each time.

Which is an odd thing for me to worry about, since I’ve never been afraid to re-write my physics and rendering code, or anything else really. Each game in the Run series was re-written from the ground up, and Runaway is its own ground-up rewrite. Though it’s taken years, I’ve learned a lot each time, and I’m building Runaway because I finally feel ready to build a standalone game engine, separate from the games themselves.

Entity-component-systems

So what sets Runaway apart from all the other engines? The big difference is, it uses an entity-component-system model. This model isn’t strictly better or worse than conventional engines, but to me it feels more elegant. (And if I’m being honest, the aesthetics are what won me over.) Practically speaking, the model lends itself to loose coupling, meaning Runaway should be especially versatile.

As the name implies, there are three important pieces here:

  • Entities are, you know, things. They can be anything from the tangible (characters, obstacles, items) to the vague (load triggers, score trackers), depending on what components they have.
  • Components are properties of entities. Things like position, size, shape, AI, abilities, and appearance. Each component is a small piece of data, and entities can have as many as needed. Note that components are nothing but passive data storage. They don’t act or update on their own.
  • Systems run the actual code that updates entities and components. Each system looks for a certain set of components, and updates only the entities with that set.

That’s the magic of the ECS model: what you are (an entity’s components) determines what you do (a system’s code).

Alice and Bob

Let’s walk through an example to see this in action.

var alice = new Entity();
var bob = new Entity();

Right now, Alice and Bob have nothing but a unique ID (Alice is 0 and Bob is 1). They aren’t characters yet. I’d describe them as “floating in a void”, but they can’t even do that because they don’t have positions. Let’s fix that.

alice.add(new Position(0, 0, 0));
bob.add(new Position(5, 0, 0));

Now that we gave them Position components, they’re floating in an endless void, doing nothing. How about a race?

alice.add(new Velocity(0, 0, 0));
alice.add(new Acceleration(0, 0, 1));
bob.add(new Velocity(0, 0, 10));

With the addition of new components, they immediately spring into action! Bob takes a commanding lead, at a speed of 10 units per second. Alice, meanwhile, accelerates slowly at 1 unit per second. Even after 5 seconds, she’s only moving at half Bob’s speed and has traveled only 12.5 units, compared to Bob’s 50. Bob will continue to widen the gap between them over the next several seconds, but his speed is fixed. As Alice continues to accelerate, it’s only a matter of time before she overtakes him.

But where is the code for this? Alice and Bob’s positions are changing every frame, even though Position, Velocity, and Acceleration components are nothing but data. It’s because Runaway has a class called MotionSystem, that was waiting all this time for these components to show up.

class MotionSystem extends System {
    @:update private function accelerate(acceleration:Acceleration, velocity:Velocity, time:Float):Void {
        velocity.x += acceleration.x * time;
        velocity.y += acceleration.y * time;
        velocity.z += acceleration.z * time;
    }
    
    @:update private function move(velocity:Velocity, position:Position, time:Float):Void {
        position.x += velocity.x * time;
        position.y += velocity.y * time;
        position.z += velocity.z * time;
    }
}

For those unfamiliar with Haxe, these are two “functions” named accelerate and move. The accelerate function takes three “arguments”: acceleration, velocity, and time, and it modifies velocity. The move function takes velocity, position, and time as arguments, and it modifies position.

Because these functions are marked @:update, they will automatically run once per entity per frame, but only if the entity in question has the correct components. Ignore time (it’s always available), but the other arguments must match. That means having a Position component isn’t enough, because move also requires Velocity. (Just Velocity wouldn’t be enough either.)

Now that Alice and Bob have both components, the move function automatically updates Position each frame. Alice also has an Acceleration component, so she meets the criteria for the accelerate function, and therefore accelerates as well.

While the physics described here aren’t anything impressive, the important thing to notice is how easy it is to add (or not add) functionality to an entity. Acceleration is baked into most physics engines, but in Runaway, it’s totally optional.

This is why I consider Runaway to be flexible. I can write extremely specific code, tailored to all kinds of specific situations, and then pick and choose which to apply to which entity.

Runaway’s current status

As of 2021, Runaway is being used in a single game: Run 1’s HTML5 port. It has all the features needed for that simple game, and a few more, but it’s also missing things I’ll need going forwards.

One of the improvements I need to make is the loading system. The current system requires pre-determined levels, such as the 50 levels in Run 1. But Infinite Mode generates levels on the fly, with difficulty based on your performance in the previous level, and that requires something more flexible.

I do hope to release the engine at some point, just not for a while. First I want to complete multiple games, including at least one not in the Run series, to be sure it’s stable and versatile enough to compete with the existing engines.