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
Orientationclass 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 longerlocate 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…
Hello, I used to play the beta of Run 3 during breaks in school. Do you know approximately when it will be complete?
No clue! We’ll find out together.
Any updates on the angel missions? Right now, basically half the game is locked off to me on player03.com, as the angel missions do not work and I don’t think there’s a way to progress past them or get to the minigames.
The best way to play run 3 is by using Garo99’s definitive collection. Here’s the link.
https://run.fandom.com/wiki/Run:_The_Definitive_Collection
bruh this is the most informed response we’ve gotten in a long time.
Just a quick note, whenever I get close to my tiles in Runaway, they turn black. I tried wiping and refreshing my computer.
I have a question. The T-Tunnel can’t be selected on the map currently in the HTML5 version. But if you click on the “A glimpse of new places” achievement, it takes you to the T-Tunnel, and it works perfectly fine. So why is it unavailable to be selected from the map? And more importantly, does this mean that the other currently unavailable tunnels are already working fine? Or is there something else going on?
Always satisfying to read about the behind-the-scenes work that goes into the game! As someone who has had to code physical equations before (albeit in Python), I appreciated your approach to making satisfying game mechanics. (Will be very curious about that “how space works in Run” post.) Best of luck with your upcoming slate!
I had a feeling you’d like this post!
And yeah, it’s satisfying to see the pieces come together. For instance, making a curve that’s supposed to take one second, timing how long it actually takes, and seeing it be correct to 3+ decimal places.
Or turning on Verlet integration and watching jump height go from ~94% accurate to >99.9% accurate.
can u release run mobile levels C-5-C-9
And plan C part 16-24
H-5 to H-9
Great read! This stuff is very interesting to me. Nice to get an update on how things are going.
yeah, it’s always nice
When are you gonna release a new tunnel in Run 3
whenever the old ones have been re implemented
FINALLY someone who understands
Keep Up the good work! Don’t feel too pressured to develop the game
Hey, just reporting a bug that’s relatively annoying! I’ve been using the SWF from your website to play Run 3 while you’ve been developing the full game, and there’s an annoying bug in all low power tunnel levels where the triggers for turning the lights on\off are only activated when they are first triggered, so dying results in the triggers not working. Apparently, this also happens in the android mobile version after you updated it, as you used the same haxe project. It would be nice if you could fix this, but I understand that you are busy at the moment doing other work.
I wonder, are you still working on run 3? Will it ever get a full release (i.e. on steam or consoles)
they have quite a bit to work on, I assume they will start working again after run 1 is fixed and run 2 is playable, but who knows! I think steam is far out from now
Well, About remastering run 1, you’re also planning on remaster run 2 afterwards ?, just asking
bro dude can you pls update the version of run 3 on the website run 3 unbloked website plsplspls because im too lazy to restart my entire progress on a real website sso pls dude