Port a Unity Game to Your Own Engine: Part 20
A concrete look into a data-oriented approach to engine development.
20.0 Reformat with clang-format
It’s time to automate formatting code. The only formatter I know of that will do the vertical alignment I prefer is clang-format, so I’m using that.
Start with an llvm format:
clang-format -style=llvm -dump-config > .clang-format
Then modify .clang-format to more closely match the style I’m using. There’s a couple of features in newer versions of clang-format that would make the output a bit better, but this is close enough.
ColumnLimit: 200
BreakBeforeBraces: Allman
AlignAfterOpenBracket: Align
AlignArrayOfStructures: Right
AlignConsecutiveMacros: AcrossEmptyLines
AlignConsecutiveAssignments: AcrossEmptyLines
AlignConsecutiveBitFields: AcrossEmptyLines
AlignConsecutiveDeclarations: AcrossEmptyLines
AlignEscapedNewlines: Right
AlignOperands: Align
AlwaysBreakAfterReturnType: AllDefinitions
AllowShortFunctionsOnASingleLine: None
PointerAlignment: Left
20.1 Split play area data
I’ve been bundling play area data (like resolution) in just what happened to be the first schema I made. It doesn’t really make any sense to be there, nor is there any practical benefit. So I’m moving it to make it clearer.
20.2 System internal data remapping
Let’s look at hooking up the hero. Start by creating the hero_instances sheet from a copy of enemy_instances. Delete what we don’t need for heroes. Which turns out to be just about everything. Hero instances are handled differently than enemy instances since they aren’t totally pre-determined at build time. But we do need to constrain the maximum number of hero instances (16). This gave a situation where the binary file is only partially sourced from the xlsx data. Some of the data (like hero positions) aren’t defined there and are just zero-initialized space. So quick fix-up of export_bin to support that.
Looking at the hero data, we have a level_wave remapping table which just points to exactly the same instances for every wave. While that would work, it’s clearly data that isn’t needed and is only there because the only example we had until now (enemy_instances) did that remapping. So instead of mapping to that format, let’s actually just do that remapping internally to enemy_instances and every other system can just get a table that’s already pointing to the correct instances wherever they might be. In the case of hero, it’s just always the same set; in the case of enemies the remapping needs to get fixed up whenever the wave changes. That gives a first use-case for using the remapping feature at runtime by fixing up the offsets.
Next up: Remove any of the previous code that did the level_wave to instance lookup and just point to the ready to go map instead.
20.3 Finish re-mapping
Complete the re-mapping and remove all the code that uses the level_wave to instance lookup.
20.4 Merge enemy_instances and enemy_instances_update
Now that enemy_instances has a runtime mapping and is no longer read-only, there’s not much benefit from keeping enemy_instances and enemy_instances_update data separated. So merge those for simplicity.
20.5 Add reset flags for stateful systems
As a bit of cleanup, to resolve the issue of initializing any data on the first call, add a reset flag to the wave update system. In general, it’s useful for stateful systems to have reset flags (as well as enable flags, aka feature flags). Also add a reset to game_state, and allow that to cascade to the wave.
20.6 Hero instances
Hook up hero instances based on enemy instances. Create an update function which just initializes one hero. And a draw function. Bullets should be handled automatically if the source data is mapped correctly.
Fix an issue with the hero_instances schema. (Not actually assigning a capacity.)
Change a constraint. The hero bullet update rate is very high, which spawns a lot more than the expected constraint of 255 maximum per wave. Adjust the allowed count to 16 bits. But all the same, the rate is probably too high anyway, so reduce that to be balanced again later.
Use the mouse position to set the position of the first hero instance.
20.7 Revisit bullet angle
Up until now bullet angle was implicitly along the vector between the root of the source and the spawn position of the bullet. But in the case of the hero, we want bullets where the offset position and angle are distinct. So we’re going to adjust the bullet format to include a base position. Adjust hero bullet and enemy bullet xlsx files accordingly.
20.8 Separate clocks
Note that hero and enemy should be running on separate clocks. Enemy is on the wave clock and hero is on the level clock. Create the level clock and update bullets to use mapped play clock and not be tied to wave.
20.9 Default values in schema
Previously added base_xy to enemy bullets, but I don’t need to make the input more complicated as they are all defined the same. In order to remove that, I need to be able to specify a default value in the schema.
Next: Port a Unity Game to Your Own Engine: Part 21
Links to files below for paid subscribers.