The past month of development has been consumed by refamiliarizing myself with a somewhat stale codebase, trying to refresh it a bit without breaking anything, and doing some expansion of the interface for playability. Today lets talk about the technical side. This is going to get a bit down into the nitty gritty of how the engine works, but what else is a dev diary for?
In order to let you plan out your future course, the game stores the current *and future* positions of all objects for the duration of the gameplay period. The game takes the initial state of the system as defined in the level file, does some math to approximate the proper positions of all the objects a certain distance in the future, and saves both the initial and ending positions, velocities, and accelerations. And then does it again using the new end points as the new end states.
Now, exact equations for the motion due to gravity of more than two objects doesn’t exist, save for some limited cases for three objects. As a result what I’m using is an approximation. A pretty crude one in fact, as I am neither a physicist nor a mathematician. The upshot is that there is unavoidably a degree of error in the calculations. You can improve accuracy by making sure the change between each state is as small as possible, which means less time between each step. There is thus a natural tradeoff between the speed and accuracy of the simulation.
For these reasons the simulation has an adaptive timestep. Objects in more extreme conditions, such as Jupiter’s moon Io, require more accuracy or they’ll be shot off into space quickly. Io in fact requires a simulation step a bit faster than once every ten seconds to keep acceptably accurate. Other objects, like Jupiter itself, might demand a step every few weeks or months. Because storing states for a long play duration has noticeable memory considerations the entire simulation doesn’t run at the rate of the most demanding object in it, instead each object is updated on it’s own clock as needed. This results in thousands of Io states being stored for every Jupiter state that is calculated.
What’s changed this month is how the required timestep for each object is determined. Before, each object would remember what the last step size it used was and start with that, then if the amount its velocity changed in a step was too big or small it would discard that step, adjust the step size, and try again.
This worked well until player-planned maneuvers came into the picture. When a player inserts or removes an order from a ship’s timeline the ship has to discard all it’s states following that order and recalculate them. If these orders are high thrust they can also affect the required size of the timestep for that object. On top of that, objects do not store what the timestep they were attempting to use was at a given past state, they only know what the last timestep they calculated was. That bit of ill-advised state combined with loose bounds on what defined an acceptable step meant that if you added an order to a ship and then removed it, the states following the time of the order after it’s removal might not exactly match the states that existed before it was placed at all. This has the potential for unintended and hard to resolve inconsistencies, and could have become a real problem.
Now instead of having the old recursive search for an acceptable timestep with all it’s extra state I have a targeted change in velocity per step, and the timestep duration is determined exactly based on that. The presence of orders can necessarily impact the step size. If there are orders for an object, it will first check and see if there are any within it’s desired timestep after the initial calculation. If not, it proceeds as normal. If there are, and the desired step size is larger than the minimum step that is allowed, it’ll cut the step short so that the next step starts at the exact time of the next order. If the order is inside the window defined by the minimum step size then the acceleration from the burn is added to the acceleration from gravity to determine the desired timestep size. If the burn is longer than the desired step size it will be divided up into multiple segments so and the process repeats until the burn is complete. When an order is removed from the list that object’s states past the step preceding the order are removed from it and everything is recalculated as if it was never there, avoiding any inexplicable footprint on the simulation.
Thanks for reading. Next time will likely go into the current state of the GUI and how the race prototype currently plays, and where it’s going from there. For now, I’ll leave you with a screenshot of an Io encounter being arranged. Click to see the full size version.