Post-Jam Update
Since the original game jam ended, I've been working on updating this project here and there. First, to fix some particularly glaring bugs in the original prototype and to add a few more features to make the whole thing feel more "complete," but also to experiment a bit with expanding the project to see if it's worth developing further. A fresh build will be available soon (after the jam results have been announced), but in the meantime here are some largely disorganized thoughts about what needed fixing, and what may be coming in the near future.
The majority of the frustrating bugs in the original prototype were related to physics and movement. The movement system was more or less like a collection of springs (though to make the whole thing easier to tune most elements don't actually simulate springs). One element pulls the ship orientation towards the nearest average surface normal, orienting the ship to the nearest surface. Another one pulls the ship's velocity towards the maximum or minimum speed according to the player's input. Having built systems like this in the past which relied heavily on a true physics simulation, in this case I opted for an almost entirely physics-independent solution. On the one hand, this is great because you don't have any odd extra forces pushing your player around, you can create totally unrealistic drag and collision effects according to gameplay needs (drag works differently in different directions, when the player collides with an enemy their speed is modified but not the direction of their movement, and it's impossible to move backwards even as a result of a collision).
On the one hand, this makes a lot of things really easy to tune and reason about. It's only as complex as you make it. However, there's always the risk that as you build more and more features and develop general solutions to different classes of bugs...well, you end up just writing your own (horrible) physics engine. I tried to ignore this and find simple workarounds for a number of particularly stubborn problems, but unfortunately in some ways it just can't be avoided. For example: head-on collisions.
Head-On Collision Handling
The original version of the game employed a very simple rule for resolving collisions with the environment: if the movement on this frame would cause the player to penetrate an object, then find the reflection of the player's velocity from the surface and, instead of following the velocity for this frame move in the direction of the reflected vector. For any kind of glancing collision on a smooth convex surface (which is luckily the majority of the collision cases) this works fine. However, when the velocity vector is parallel to the surface normal, the reflection just points backwards, so you get stuck as you oscillate between moving forward slightly and moving backward slightly. Even worse: another rule for the ship's movement was that the ship must always be moving forwards, so even the small backward step was impossible.
What I really wanted was for the player to be able to slide around collisions with static geometry in the environment. I have considered damaging them if they hit something head-on at a high enough speed, but in a game where players may be moving at 1000km/h it might be a little unfair to kill them just because they hit something at an unlucky angle. Now, the reflection approach above works fine for sliding on many surfaces, and if this were the kind of game where the only environmental collisions happened at the edge of the track (think a regular car-based racing game, for example, where you might only collide with the left and right edges of the track and never head-on) then it might be sufficient, but this game contains both flat surfaces the player might ram into face-first and a handful of concave pockets they can wedge themselves into. Solving both of these to allow the player to slide around them without getting stuck is tricky, and the final solution is not that different from what you might see in the collision resolution of a real physics engine (but, you know, we're pretending there's no such thing as friction, etc).
When a penetration is detected, before the ship is allowed to continue its movement for this frame we first try to move it to a safe location away from the penetration. This should be place nearby which is free of penetration with any object. Ok, easy, just move backwards. If you weren't in penetration when you started, then you can not be in penetration if we just move you back slightly. Great, but now where do you move to and how does the ship's velocity change? If you just step back and declare the problem solved, then on the next frame you'll move back into the same penetration again. So we try to slide parallel to the surface in a direction as similar as possible to the remainder of our velocity for the frame. But... what happens if that movement now puts you into penetration with something else? And here's where you start writing your own (horrible) physics engine without realizing it XD
So, a major update to the game was a much-improved sliding collision resolution scheme, but with some simplifications. We try to resolve the collision simply by sliding along the surface, and if this works the player's velocity is basically unaffected (they may change direction for a few frames until they slide past the obstacle). However, there are some cases where this fails. In an extreme case, consider flying face-first into a flat wall. We get into penetration, step back a little bit and then want to slide along the surface. Which direction do we slide in? If our velocity is collinear with the surface normal, then there is no single sliding direction which is closer to our velocity vector than any other, so it's kind of undecidable. Similarly, if we hit some concave pocket and our slide brings us into penetration with an adjacent wall, we may find that we just cannot slide in our desired direction. In these cases, the player's velocity is reduced to just cover the distance they moved before penetration (which in these cases is usually a very small value), and at this point the player can then change direction themselves to get around the obstacle. This penalizes the player for crashing, so it's important that this not happen due to some accidentally-placed bit of invisible level geometry, but as long as it's clear to the player why they crashed I think it's a fair trade-off.
Movement & Controls
The original ship movement code operated with a pretty simple set of goals:
- Align the ship to the nearest (average) surface normal
- Keep the ship (close to) a fixed distance from the nearest surface
- This actually was implemented as a typical spring-damper system, which sometimes resulted in crazy results if you hit a surface too hard or the nearest-sampled surface changed quickly.
- Acceleration was based on a kind of "throttle" input: as the throttle from the player increased, the acceleration would increase.
- This sounded cool, at the time, but seemed a bit frustrating and unintuitive to enough people it needed to change.
- This sounded cool, at the time, but seemed a bit frustrating and unintuitive to enough people it needed to change.
These were all rewritten significantly, and some additional rules added. Now, the ship's movement goals are:
- Align the ship to a weighted average normal, with weights influenced by the current desired movement direction and distance to each sample point.
- This change was important for two main reasons: First, if the player wants to move to the right, it becomes frustrating if the ship suddenly aligns itself to a surface on the left. Second, with some other physics/movement changes the ship is now often airborne, making it easy to jump from one surface to another. If we try to stick to everything we see, though, it's easy to end up spinning way too much as we realign to several very different surfaces in quick succession (e.g. the ceiling and floor when the ship is an equal distance from both).
- Keep the ship (close to) a fixed distance from the nearest surface
- Acceleration & braking now works more like most racing games
- A "gravity-like" force is applied to the ship along its local vertical axis. This facilitates "jumping" off of ramps regardless of the ship (or ramp) world orientation. Having no gravity means many ramps would just launch you into space, and applying gravity in the world frame would almost certainly be very frustrating for many players since the track is constantly rotating all over the place.
Combined with the improved collision resolution, the ship movement and controls now feel much more fluid and much less frustrating. They still aren't perfect, but they're getting close to something I'm happy with.
One additional constraint on the original movement system was that the ship could not turn left or right. This is why the track only goes in a straight line: if you tried to turn left the ship would become unstable. This issue has also been resolved, opening the way for much more interesting and complex track designs in the future.
Track Generation
As mentioned in the post-mortem, the track generator was basically random which led to a basically random experience. Sometimes the game would feel too slow because the speed boost powerups were very sparse. Sometimes the game would be impossible due to the arrangement of spawned obstacles. Additionally, some of the rules for when to despawn segments of the track could lead to a segment disappearing before you had crossed it, creating an unexpected challenge ;)
The original track generator basically had two parts: a distance-based ruleset which would spawn different track segments based on how far the player had traveled, and an overlap-based system for level geometry which would despawn segments after you had passed through them. The distance-based constraint was a fine starting point. It allowed for easily specifying different map segments at the start of each run vs. later, but since there is no maximum distance the player may travel it's doomed to always devlolve into just choosing random from whatever set of segments has the highest distance constraint.
The current solution I'm working on is to mark up each map segment with some meta-data about its contents. E.g. one segment may contain enemies that shoot projectiles, a certain kind of obstacle (like a gap you must jump) or may provide a transition from one kind of segment shape to another. These maps are then fed into a pattern generator which applies some constraints, for example two of the same kind of obstacle may not appear in adjacent segments, or a certain kind of transition may not appear above or below certain elevations. The goal here is to produce several pattern generators which will produce different kinds of sub-sequences of track segments, which should all be passable, have a consistent difficulty, etc. The final track can then be generated on the fly by chaining together several of these pattern generators, with some additional simple constraints on which order they should go in (e.g. the difficulty should increase over time, but in the end you don't want non-stop super-hard sequences, so even late in the game some easier sequences should be generated from time to time to give the player a break).
It's still a work in progress, but the system already gives much more control over the shape of the final track. It will still take a lot of testing and some new track segment designs before I can confidently say it produces results better than the pure random approach, but at least the potential is there ;)
With the added flexibility of the new track generator, I'm interested to start to explore creating more complex levels.
Other Stuff
The systems above have been the focus of the improvement efforts so far, but there are some other assorted updates coming. In particular, a proper scoring system (based on your distance traveled, time spent, and overall speed) and high scores for each track, an assortment of small graphical improvements, additional sounds and music, and an overhaul to the way the speed boost powerups work (may be worth a short article on its own in the future, but the gist of it is that they were reworked with the goal of making their effects more apparent to players). Gamepad support is also much improved (it was there before, but the original prototype really played much better on a keyboard).
Get LXOR
LXOR
Faster and faster
More posts
- Minor update: Beta2aOct 23, 2021
- Beta Build AvailableOct 17, 2021
- Jam PostmortemSep 07, 2021
Leave a comment
Log in with itch.io to leave a comment.