Posted On: 2021-07-19
As promised previously, today's post is about the new flight system I am prototyping. While I'm not quite as far along as I'd hoped to be at this point (pardon the placeholder art), I am confident that what I have is both interesting to read about and likely to appear in the final version of the game (albeit in a modified form.)
Going into the problem of designing the flight system, I knew I wanted something that "felt" better than simply translating over x and y. My prior attempts had exclusively used translations, and it was a constant struggle just to get something that felt even vaguely pleasant to use. Instead, I took inspiration from how (simplistic) vehicles handle in games: the character always moving at a constant speed with the player "steering" the direction of that movement. The first implementation was immediately promising: limited turn speed raised the skill ceiling, and, even though the speed values could use more iteration, it already felt better than anything I'd previously attempted.
This first implementation was isolated from all the other systems in the game - including the physics system. I knew I needed to change that somehow (otherwise players could fly through walls unimpeded), but I didn't particularly like the idea of the character losing all their speed the moment they hit a wall. Steering away from an obstacle after colliding with it always feels like a chore, so I thought about whether I could automate that in a way that retains the character's speed throughout. Eventually, I settled on using physics to "push" the character away from the wall - so that anytime they might collide, they are instead deflected to an angle that doesn't cause collision. The result felt quite nice: flying straight into a wall wouldn't stop character movement - instead, they'd quickly swerve until they were flying almost parallel to the wall. Which, of course, gave me an idea for how to improve it further.
"Almost" parallel was not good enough: it would be far more interesting (and fun) if the character impacting a wall instead followed along it perfectly, automatically taking any twists and turns without losing speed. Additionally, since the game's design already encourages players to modify the world's obstacles (moving and shaping them as needed), being able to fly parallel to a wall that the player themselves created would open all kinds of satisfying possibilities. Unfortunately, implementing parallel movement turned out to be much trickier than expected.
In theory, one will move parallel to a line as long as one's forward momentum is at the exact same angle as the line*. At first, I thought I could extend that to curves: so long as one's forward momentum is the same angle as the curve at that point, one would follow that curve. Unfortunately, doing so precisely would require sampling an infinite number of points on the curve - anything less would result in an imperfect imitation**. Over time, the character will slowly drift away from the curved surface it was following. For many shapes the drift was subtle enough a player might not notice, but for extremely round surfaces (such as circles), it was immediately obvious something was wrong.
Unfortunately, I spent quite a bit of time trying to fix/workaround this issue. Only when a friend suggested trying to follow a path (rather than sample the curve in realtime) did I get the push I needed to become unstuck from this problem. A curved path would, in theory, always be parallel, as it could be defined from the same number of points as the source curve. Furthermore, a path would give me a lot more design flexibility: with a path I could define how far away the character should be from the surface, as well as potentially changing the path to smooth out/modify the curvature in tricky areas (such as rounding out right-angle turns).
Making a path from the intersection of arbitrarily shaped, scaled, and rotated in-game objects is a tricky problem. Fortunately, it is also a "solved" problem: with the right mix of libraries and tools, I don't need to know how to do it myself. Unity's built-in Composite Collider can take multiple individual colliders and automatically get their combined shape (as a polygon or edge loop). While this is primarily used to improve performance of the physics system, it also provides a GetPath function that can get a series of points defining the exact shape of the physics collider*. I can then feed that path into the polygon editing library Clipper, to generate a new path at an offset from the original one.
The next step after getting the path is to follow it - and this is the specific problem I am currently tackling. I have a naive implementation up and running (by sampling the path every physics step, and warping the object to the new location), but I expect this will require revision/rewriting as other systems begin to affect the flying character. Nonetheless, this simple implementation validates that following a path looks and feels great - whether that's spinning around a circle, or zipping along the edge of a complex mash-up of (placeholder) geometry.
There's still a lot left for me to do before flying is usable in-game. Following the path is technically separate from the actual flying controls, so I need to blend between the two seamlessly. Both flying and path-following will likely need additional tweaks to account for hitting a hazard/enemy, and I'll need to iron out the details of what speeds are available (as well as how to let the player influence/pick the speed they want.) Animations and visual effects are also required - though those will likely be kept minimal for now. Lastly, and perhaps most importantly, letting the player swap between normal movement and flying likely means changing both systems to make them play nice with each other; a task that I expect will be more difficult than I anticipate - and I anticipate it will be quite difficult.
Fortunately, most of these are things I hadn't expected to be done one week after ideation - only the integration of flying and path-following could be considered "late". Getting those to play nice with the existing character implementation was considered the next goal - one that, even if everything went perfectly, had no way of being done in such a short time. Going forward, I expect to keep to that pattern: it's often valuable to tackle the toughest parts first, and, at this point, bringing the separate pieces together is both the hardest and most immediately rewarding task. Here's hoping that it will all go smoothly, and players will (one day) be able to experience the joy of flying along winding surfaces in my game.