As there is currently no proper PDF manual for Myriarch, here are some noteworthy implementation details. This post originally covered the .NET version (released 09/2012) but has been revised for the new Java version (released 06/2017).
The core simulation uses the
QuadTree class of my Tektosyne library to locate units and their neighbors. During each time slice of 100 msec, we loop through all units to obtain orders and execute them. Orders are either the division default orders (e.g. march north) or individual unit AI decisions when enemies are near. “Near” means within combat range during one time slice, given movement speed and weapon range, plus whatever extra range we can afford in terms of processing speed. (That’s going to be a problem for missile weapons!)
Once a unit has been involved in combat, it is considered to have dropped out of formation and will look further afield for enemies. The distance correlates inversely with the number of friendly nearby units, so as not to overburden the
QuadTree search. Unit AI engages any discovered enemies in combat if morale is above a minimum level, and runs away from them otherwise.
Order execution consists of turning & marching into the desired direction until we’re either close enough to perform an attack, or a collision is detected. We always resolve collisions between two units only, picking the nearest one in our path and ignoring all others. Any collision terminates the current unit’s movement for the time slice, and changes future direction & speed for both colliding units. The current unit typically loses a bit of leftover movement capacity, but time slices are short enough relative to unit speeds that this is acceptable. The other unit will do its own collision processing later in the same time slice – possibly again colliding with the first unit, but using the newly adjusted impulses.
Compared to a textbook billiard ball simulation, we fudge our collision algorithm in several ways. First, we assume a minimum speed of 1 m/s for both units, even when they are stationary. This accounts for soldiers actively trying to move while blocked. Second, we accumulate “push mass” whenever two units collide while moving in the same direction. This increases the total force with which the front rank will eventually push against a unit going in the opposite direction. And lastly, we add a 10 cm random displacement to unit coordinates after a collision. This proved necessary to avoid two units permanently blocking each other, and also results in a more realistic “crowd simulation.”
Within the GUI application, all simulation logic including the unit loop runs on a single background thread. The
QuadTree class is not thread-safe and may change globally due to rebalancing after insertion or deletion, so breaking up the loop into multiple threads would be rather difficult. Once the simulation thread is finished with a time slice, the new simulation state is written to a snapshot which is then rendered to a JavaFX
Canvas on the main GUI thread. JavaFX turned out to be surprisingly fast here, never lagging behind the simulation or losing responsiveness to user input, so no further explicit multithreading was needed. (JavaFX is already internally multithreaded.)
Myriarch records the entire simulation as a sequence of “events” which allow exact replays. Most events store changed unit variables such as map locations. The storage requirements for this task turned out to be rather enormous, exceeding the default JVM memory limits on a 64-bit system. Myriarch currently keeps only the most recent 100 seconds of a simulation as an exact event recording. Prior to that, only checkpoints are available, i.e. simulation snapshots created every 10 seconds, plus a smaller number of “special” events such as time slice advances.
Even so, some trickery was necessary to handle event storage. All events are backed by four integers which getter methods convert to
double as necessary.
SimulationEvent is a regular Java object but never stored as such, due to the JVM object overhead. Instead, a custom
EventList directly stores the four backing integers of events, recreating the objects on demand. Storage uses multiple arrays of 100,000 bytes each, so as to avoid having to repeatedly copy and enlarge a single backing array. Finally, those arrays are written out as Base64-encoded blocks during XML serialization.