Age Of Empires! In the browser! Kind of!

demo at the bottom

The number of spatial dimensions in modern video games is likely a stylistic or design choice. In 1997? The deterministic game simulation of Age of Empires needed every bit of processing to pathfind units, task villagers, and calculate ballistic trajectories. 2D sprites allowed several hundred objects on screen in each frame, so an orthographic perspective it is! In the years since, Age of Empires II has undergone remakes and expansions, upgrading the resolution of the sprites and audio quality and increasing the framerate of animations.

aoe2

However popular the game was, upon release and in our collective youth, the series hit a lull in the late 2000s. Although Age of Empires II: HD Edition garnered interest in 2013, the release of Age of Empires II: Definitive Edition in 2019 and the subsequent pandemic revived the competitive multiplayer scene. With professional players came professional casters and CaptureAge, a free companion software (developed by passionate volunteers) for viewing live and recorded games (below).

captureage.png Credit: Dave_AoE

The above screenshot is from the 2023 Nation’s Cup Finals. You can see the chaotic minimap among the whirlwind of other information on screen. The Chinese team is known to excel on the “Nomad” map. Streaming software is a fantastic way to immediately answer a viewer’s question about the game state instantly (although new players may need to search the UI).

Although my gameplay doesn’t match the pros' speed, precision, or tactics, the ability to see statistics and my game mistakes is fascinating. Sometimes, one may resign when the opponent is pouring on relentless pressure. Only after watching the replay could you learn that your opponent took significant economic shortcuts to do so, and you could have won if you had scouted their strategy and held on a bit longer!

Staring at tiles

So I wondered, “How could I review my games when I’m not at my computer?”. I wanted a reason to learn three.js! So, the technology of choice was a browser-based viewer with the added benefit of an additional dimension! Traditionally, players start on opposite sides of the map, and with this tool, you can spin the map to always view it from a preferred perspective. I was happy to build my own 3D models to represent the game objects, giving a whole new view of the game.

The first step was accessing game data. CaptureAge did it by reverse engineering the memory layout of the game state in real time. Since I won’t have access to that, I’ll need to use a game recording. The aoc-mgz Python library is the community tool to grab game data from a serialized recording file. This file holds the initial game state and all of the actions performed by the player. I only need the map, so I’ll grab that and export it to JSON:

{
  "gaia": [
    {
        "class_id": 10,
        "index": 1,
        "instance_id": 2718,
        "name": "Tree (Palm Forest)",
        "object_id": 351,
        "position": {
            "x": 46.5,
            "y": 13.5
        }
    },
    ...
  ],
  "tiles": [
    {
      "elevation": 0,
      "position": {
        "x": 0,
        "y": 0
      },
      "terrain": 14
    },
    ...
  ]
}

gaia are animals, resources, relics, and other neutral game objects while tiles are the ground of the map. In the game, the ground is a polygon mesh with rectangular shapes overlaid to help with building placement. However, the elevation of the ground complicates the representation. Oh, look at that rolling nature! What a great place for an outpost.

aoehills.jpg

I couldn’t find any resources for the height map used in the game, so I reversed the elevation -> polygon mapping by constructing the many tiling cases (spoiler, it’s not actually that many cases) of hills in the map editor and comparing it to the game recording output. I only recently discovered the elegance of the Marching Squares/Cubes Algorithm. It turns out I implemented a subset of the cases! This is excellent news because I can’t find the several pages of dots and boxes I built trying to figure it out, and I can use Wikipedia’s figure!

The goal here is to find the elevation of each vertex of the tiles and build a triangular mesh connecting them. Step one is to iterate through every tile and assign each of the 4 vertexes to the maximum of its neighbor tiles. This ensures all edges are connected. Next is to find the tile’s appropriate shape, one of 16 cases.

Only the top left and bottom right are important here marching_squares

I named my cases with great imagination (assuming black is the higher elevated vertex):

I can classify the shape by finding the sum of vertex elevations and subtracting the tile elevation. For example, a sum of 1 is a one-up corner! Depending on the orientation, we build the appropriate triangles:

new Triangle(
    new Vector3(col - 0.5, quadVertices[0], row + 0.5),
    new Vector3(col - 0.5, quadVertices[1], row - 0.5),
    new Vector3(col + 0.5, quadVertices[3], row + 0.5)
)
new Triangle(
    new Vector3(col - 0.5, quadVertices[1], row - 0.5),
    new Vector3(col + 0.5, quadVertices[2], row - 0.5),
    new Vector3(col + 0.5, quadVertices[3], row + 0.5)
))

col is the x-axis, and row is the y-axis. In this case, 1 and 3 are shared vertices. (three.js uses a y-up coordinate system, which is just too bad.)

With that out of the way, gaia is easy; you slap a .gltf model at the position and look up the elevation from the height map. I carved out some low poly models in Blender to represent common game objects (the sheep are my favorite). The only gotcha is to use an InstancedMesh to reduce the number of draw calls. With hundreds of trees, this was essential.

And finally,

The Demo

Overall, this was an enriching project. I love 3D modeling, mostly parametric CAD, but doing it in a way that increases the accessibility by deploying to the browser makes me happy. What makes me giggle more maniacally? Sending streams of Hunnic hussar into my opponent’s base or 20 Aztec monks to my opponent’s arena. 1v1 me sometime?

WOLOLO