Hello everyone and welcome to the newest update on openage development. This time we are going
to take a look at all the things that happened since August
and also other stuff that might be interesting. So without further ado, let's bring you up to date!
Game entity interaction
We'll start with the features that enable interaction between game entities, i.e. attacking, converting
and all other things that game entities can do to each other. I'm happy to say that this milestone
has now been completed (yay!) and also works pretty nicely with everything else we've implemented so far.
Check out this recording to see how it looks:
To recap the initial problem description for game entity interaction: The challenge in letting game
entities interact is not the triggering of a single action, e.g. an attack. That's actually the easy
part. Instead, the major challenge arises from modelling the complex gameplay patterns in RTS that combine
multiple actions. What do we mean by that? Well, take these types of gameplay as examples:
- looping actions, e.g. attacking until a target is dead
- chained actions, e.g. moving closer to another unit before attacking
- combinations of the above, e.g. chasing a moving target
You can probably find more examples if you think about your last gaming session. RTS and especially AoE2
are full of those patterns.
Agent Behavior in the openage Activity System
Another way to think about how these gameplay patterns work is to see game entities as agents for the
player. Unlike in a first-person game, where a player's inputs directly control the character, agent
control is usually more indirect. Agents may be given commands that then trigger specific behavior and
might set up a complex chain of events. They also may act completely on their own, e.g. units using the
aggressive/defensive/passive stances system in AoE2.
openage implements this agent behavior in a place called the activity system, which we already talked about
before. The idea is that game entity behavior is modeled via a
node graph that acts like a state machine for the individual game entity. Traversing the graph happens
when the game entity receives events, e.g. a player command. Visiting nodes then executes actions or
checks conditions to branch on different paths in the graph. You could also say that the activity graph
represents the decisions a game entity can make.
Most of the work for game entity interaction went into making the activity system fit for interactive
agent behavior. For example, we implemented new decision node types to evaluate the state of the game
entities that interact, e.g. for checking if one game entity is in range of the other's attack radius.
We also had to design an activity graph that matches the unit behavior in games like AoE2, so that gameplay
feels the same. You can see the resulting graph below:

Follow the graph from the Start
node on the left. The behavior for attacking is located in the
top right portion of the graph. You can see the basic loop for checking whether the target is in range
as well as the application of an effect (like attacking) via the ApplyEffect
node.
Additionally, we wanted to make the whole node graph configurable, ensuring that game entity behavior is
changeable instead of relying on hardcoded behavior. This actually worked pretty nicely. We managed to expose
all node types, decision functions, and actions via the game data files, so almost everything can be
configured without changing the code. Currently, there are no failsafes for checking if a graph is "correct"
(i.e. it doesn't crash the whole engine), but that will hopefully be added in the near future.
Targeting game entities (with your mouse)
So far we've only mentioned how agent behavior may be triggered by commands and conveniently left out
how these commands end up in the game. We want to specifically look at the implementation of
targeting game entities, i.e. finding out which game entity a player "right-clicked" on.
Player inputs like right-clicking are routed through openage's input system. A minor "problem" for
interaction is that this input system knows almost nothing about what's going on in the simulation.
This design allows us to run the input system in a separate thread, making the handling of inputs
faster and easier, so we ideally don't want to change it. However, it raises the question: How do we
get to know what game entity a player right-clicked on? After all, we should be able to give the
selected game entities a hint on what they should interact with.
openage's current solution is a bit janky, but it currently gets the job done. For this, we consult
the openage renderer - another engine subsystem. The idea is that the renderer creates an additional
texture, a so-called ID texture, that writes the value of the game entity ID to the pixel location
of sprites belonging to said game entity. Essentially, this texture lets us look up which game entity
is occupying each pixel on the screen.
The ID texture is directly passed to the input system which then uses the (x, y) coordinates of the
click event to look up the game entity ID at that position. Unlike the color textures created by the
renderer, the ID texture is never shown on screen. Here is what it would look like if it would be drawn:

Black pixels match no game entity. Colored pixels represent locations of game entities.
Side Tangents
Aside from game entity interactions, there were a bunch of features that were implemented on the side.
These features are probably only "cool" if you are a giant nerd (like @heinezen),
but he writes these posts and decides what gets discussed, so...
Curve Compression
Curves are openage's internal data
structure for storing value changes over time in the game simulation.
In other words, they store past, present, and predicted values of game entity data used during the game,
e.g. HP or unit position, as time-value keyframes. Values for the time between keyframes can also be
interpolated.
In most situations in the simulation, keyframes are inserted lazily, which means that the simulation
doesn't check whether the keyframe is redundant or not. This is usually fine and also desired, since
this makes operating on curves much faster.

Curve without compression. Keyframes B, D, E, H, and K are redundant and don't change the
interpolated values.
However, there are cases where we don't want duplicate keyframes. An example of this is the usage
of curves for storing the animations of objects in the renderer. To which frame index to use for
the current animation, the renderer checks the animation's keyframe time to determine when the
animation started. The frame index is then roughly calculated using this formula:
| frame_idx = (current_time - keyframe_time) / time_per_frame
|
Problems can occur when the animation is triggered by a looping action, e.g. attacking a game entity
until it's dead. Currently, the animation is requested for every "iteration" of this loop, i.e.
every time the action is done, a new animation keyframe is inserted. However, if the loop time
for the action does not match the loop time of the animation, then the animation gets abruptly
cut off whenever a new keyframe is inserted.
Curve compression solves this nicely by only adding keyframes is they don't change the interpolated
value. This is what the curve looks like when compressed:

Curve with compression. Only the necessary keyframes remain.
This not only solves the jittering animations in the renderer, but it's also useful for other use case
where we want the data to be compact. Examples for this are curves that are sent over the network
or recordings written to disk.
Converter Cleanups
Transforming the game data from AoE2 and other games to openage formats is handled by the openage
converter. The code for this conversion is actually pretty complex and rivals the actual engine
code in size. However, it's also been infamous for containing a bunch of files that are pretty
much unreadable as they contain up to 10,000 lines.
Having huge files is not a really a problem for running the scripts, but rather developing on them.
Turns out parsing giant files doesn't make the job of IDE indexers very easy. That alone was enough
motivation to finally split up the converter files more. Increasing the readability is also
a nice plus and makes the converter more maintainable.
As a side goal, we've also been working on making the converter less error-prone and more forgiving
in some cases. This was necessary due to the constant changes to the game data format in the new
expansions for AoE2:DE. Updates to AoE2:DE should hopefully not crash the converter as much anymore,
although we still have to keep pace with the data format changes.
Outside Contributions
Since our last update, openage has received a number of outside contributions that we want
to honorably mention here. These add a few significant improvements to the engine :)
@jere8184 and @dmwever improved
the openage pathfinder. Most notably are the increased the pairing heap performance,
the introduction of the dirty
flag for integration
as well as the addition of an array curve datastructure
for use in the field types.
@jere8184 also added fixes for the
Windows builds and
CI pipeline. They also introduced fused
types to the SLP/SMP/SMX/SLD image conversion in our converter.
@ZzzhHe has made added several new features to the renderer:
- Completeness checks for uniform initialization
- Binding empty vertex array objects
- Configurable shader templates
@haytham918 had added optional clang-tidy
checks to our buildsystem.
@bytegrrrl added a few operations to our FixedPoint
class
that enable us to use pure fixed-point calculations in more places. FixedPoint
can now
also use an intermediary type useful for operations on large numbers, thanks to the
contribution of @Eeelco.
We hope we didn't forget anybody who made important changes :) More external contributions are also open
in the GitHub repository.
What's next?
A better first question might be: When does the next blogpost release after this one? Will it also
take almost a year? That's a good question, although the answer is that sadly it will probably take a
while to write the next update. Writing the devlogs takes a lot of time, especially when they
should be understandable for newbies. Also, when the choice is between writing devlogs and coding,
coding is still much more fun... So coding will take priority for a while. However, I (@heinezen)
still like creating the devlogs, so maybe they will pop up more frequently again sooner or later :D
As for openage, we now have opened a loooot of possibilities with the completition of game entity
interaction. We can now go the boring route and add more interaction types that are not handled
fully yet (like building construction). There's also the possibility for a more exciting route
which leads us even deeper into topic of state transitions, e.g. proper handling of unit deaths.
Or we might do something entirely different depending on who has better ideas. I guess you'll find out
next time ;)
Take care!
Questions?
Any remaining questions? Let us know and discuss those ideas by visiting our subreddit /r/openage!
As always, if you want to reach us directly in the dev chatroom:
- Matrix:
#sfttech:matrix.org