I basically learned to code thanks to a few helpful and extremely patient IRC members in the Spring RTS engine[1] community (some of whom I spot in these comment sections on occasion - hi!), and deterministic lockstep simulation for online play was one of the first ideas that totally blew my mind when I learned about it ("b-but that means all the "random" numbers on every computer need to be the same! and so do all the floating point calculations across different hardware!"). Of course, to me at the time, that was deep magic that I thought I'd never be able to fully understand.
[1] see https://springrts.com/ - or check out http://zero-k.info/ and https://www.beyondallreason.info/ for games on the engine that are still pretty active
> At first take it might seem that getting two pieces of identical code to run the same should be fairly easy and straightforward
I found that a little funny. I would certainly not assume that. Synchronization problems are one of the scariest areas of programming in my opinion.
The biggest downside is mentioned in the article: If one person lags, everyone lags. In an 8-player game, the chance that at least one person lags was pretty high. I used to play on the Mac version of GameRanger, a small community. That was 2008, when reliable internet wasn't as common. Players had personal reputations, and those who lagged too much were often excluded from games.
I think that internet multiplayer is possibly one of the hardest things in game dev (use an engine that gives it to you!!!) despite feeling like some little sideshow.
I imagine Blizzard and Westwood have similar stories from the 90s.
there's this for Warcraft 1 development
I don't understand this. Running the whole simulation locally on both ends means that a modified client would have access to the whole game state, and I don't really see how you could patch that out.
Anyone have any idea what they actually did? Try to detect modified clients? Obfuscate the game state to make it harder to interpret?
I worked at MacSoft during the Age2 and Age3 days. The ports were faithful to the windows versions, but cross platform hadn’t been solved at the time because of this problem.
It also made long game play sessions longer because the calculated state kept getting more complicated.
There was one particular Mac OS X update that broke math interoperability for multiplayer because they changed how math worked on the OS. This meant we had to bundle a common math library to ensure the game the game states would line up, preventing unnecessary CRC checks.
The model would crash the game (and world editor, that's why we have to spawn it during runtime) when displayed, but it wouldn't get displayed when under fog of war, so you'd put it in a place that is impossible to be seen by a player under normal circumstances. But if someone uses a fog of war cheat or a maphack, it'd crash for them.
Of course it won't prevent you from more advanced hacks which e.g. modify the client and display an overlay of the enemy units rather than just revealing the fog of war.
I take it as a sign that I'm in a good healthy place mentally when the most stressful and anxiety inducing thing my brain can conjure up is me being rushed by 3 units in the dark age.
So as much as this is a fascinating piece of history and an impressive technical solution to the constraints of the time, I think modern games ought to move past it.
But why? As far as I know mega-hits like Warcraft III and it's new, updated, version, "Warcraft III: Reforged" which came out 20 years later still use that technique.
The benefits do go well beyond being able to "send" hundreds of units across the wire: a deterministic game engine allows to create tiny replay files and, very importantly, allows to find and smash bugs way quicker.
Having the next game state being a deterministic function of the current game state + player inputs is great.
What would RTS games win by "moving past" that? To do what instead? How would you then implement the replay functionality? You'd also invariably run into a class of bugs which would be hard to reproduce but which would be trivial to reproduce using a deterministic engine.
From a latency point of view you're not gaining anything either: you need to receive the other player's units position anyway. So what's the difference between receiving the hundreds of unit's position or receiving the player input that created these unit's position? Just compute them, deterministically, as soon as you get the player's input.
The subject already came up here on HN and some posted about games using deterministic engines before AoE.
The reason I did it is I had a bug which happened ultra-rarely and couldn't figure it out so I thought a long time about this and realized a could make the game deterministic and that would maybe allow me to record the bug happening and then be able to replay it. I found that by myself: back in 1991 I had never heard of anyone writing a deterministic engine back then. So I took a few days and rewrote the engine to be fully deterministic. Sure enough it eventually caught the bug: some case where the hero would clear a level after having fired two shots at once, which was an extra (by default it only had one shot at any time). The shot still on the previous level would continue to "live", invisible, in the following level, and would corrupt the memory. Classic.
Oh the memories to see that article again!
> Game Developer was first founded in 1997 as Gamasutra and has strived since its inception to be a leading resource and reference for game development and industry knowledge. Following the shift from Gamasutra to Game Developer in August 2021, the site has maintained that mission while embracing the in-depth content its namesake Game Developer magazine is known for.
(Same for the NIPS conference. But oddly not for the science journal PNAS.)
For example, being able to micro manage archers to dodge other archers, and time their shots is something that wasn't possible previously.
- The AI system is not part of the deterministic simulation. This was surprising to me, and after contacting one of the original programmers it was explained that it was due to a desynchronization bug that the "AI and network programmers weren't able to fix it in time".
A consequence of this design regression is that, due to the AI now being authoritatively run by the designated host player, network congestion issues arose which lead to a clear series of progressively more aggressive optimizations to reduce egress traffic. This primarily consisted of a very simple filter (mentioned in the article) which dropped duplicate commands in the common submission path, meaning it applied to both local user and AI commands, along with batching of AI-submitted commands which would be flushed at rather arbitrary times.
I'll note that I've restored AI being deterministic in my project.
- A rather obscure determinism bug resulted from their compiler's implementation of a few CRT routines, namely fsin/fcos and a few others, which leveraged the specialized ISA instructions of the same name. The problem being that these transcendental functions are beyond the scope of the IEEE 754 spec. and ergo are hardware implementation-dependent. In practice, contemporary Intel/AMD chip families produce bitwise the same result, however those around the time of AoE are known to diverge on results to some small margin (as confirmed by an Intel engineer on a thread I came across while researching).
- The game employs a dirty-update system for rendering, not only that it's at a scanline granularity. This was something I was very pleased to see, as it's a exceedingly rare to see such an important optimization in games of this era (although common in earlier eras)
- There's some "interesting" naming conventions, one being prefixing member variables names with "value" -- there's even a "valueValue". Very little consistency in general in this regard, a reflection of independence between teams working on different components.
- While there are attempts at validation of input into the simulation (albeit woefully inadequate), it relies on ad hoc inclusion of PID (player ID) fields within commands. This is entirely useless, as this information is not authoritative and controlled by the players, permitting them to "spoof" the contextual information required for validation.
This is one of the more perplexing aspects of the engine, especially given the necessary information about the origin of a command is ofcourse available.
(As this information was later publicly published by other individuals I don't see a problem with elaborating on it here as I have)
- An example of missing the wood for the trees: session information goes through an ad hoc compression for its wire form (just bitpacks fields) to conserve bandwidth, however the architectural choice is to synchronize this session state by just having the host broadcast the state --dirty or not-- every 200ms, flooding the network pointlessly (atleast in the 18.8k days)
- While Age of Empires is somewhat notorious for its poor multiplayer performance (notably when contrasted with say, the recent "Definitive Edition"), and while its implementation of this synchronization model is certainly rather juvenile, it should be understood that the final MP gameplay issues are primarily due to the choice of a peer-to-peer topology over the public internet, which at the time was the most reasonable.
While superficially P2P may seem like it should achieve the lowest latencies for instance, the reality is that the primary determinant is the characteristics --jitter, delays and packet-loss, reordering, ect-- of the path between two hosts and a P2P architecture means there's n*n paths (network egress/ingress paths are typically asymmetric). In contrast, a server/client model you not only have far fewer routes, but datacenters are located at critical points in the network, roughly analogous to comparing travelling A->B along a freeway versus via the maze of residential streets.
- The state checksum "algorithms" involved are bordering on useless, atleast for state-tracing (such as when debugging desync. bugs.). They appear to have been devised by way of believing doing "a bunch of random bitwise ops" constitutes sufficient mixing -- a quick test demonstrated that csum collisions were not just possible, but occured sometimes for over 80% of inputs.
--
There's a lot more that could be said but I feel that's enough for now.
The thing is that this is perfectly reasonable if your network infrastructure is known. If you have fixed bandwidth and deterministic packet sizes, then you can do math and know what the behavior will be. Determinism is good! Also this assumes the network is single purpose. Which it was! For games in the 90s it was! This isn’t bad design, it was good design for the network infrastructure people would have had at the time!
I've been told the source code for it leaked some long time ago and has floated around for the past two decades or so. While most people contend that EA had lost the source code to the Command & Conquer series games many years ago when the studio was closed down and assets were shipped to "DICE" in Stockholm
Are you the OpenAge author, or maybe Voobly? I've never read such detailed info on AoE's internals online. Thanks for the post.
> In practice, contemporary Intel/AMD chip families produce bitwise the same result, however those around the time of AoE are known to diverge on results to some small margin
That little bit of validation I needed for why I avoid AMD CPUs. (not entirely serious)
Actually suffixing! i.e. objectsValue.
https://zoo.cs.yale.edu/classes/cs538/readings/papers/terran...
Maybe the link can be replaced? Also can (2001) be added to the title?
>>sarcasm overload
Install uBlock Origin yo