I’ve been writing about implementing A* in ECS to see if it’s faster. See here and here. It is indeed faster, way faster! I’m too ashamed to tell you that our old OOP A* is so slow. It runs on the range of 2ms to 50ms. This is already using Jump Point Search and multithreaded. It badly needs optimization. I want to use the ECS implementation, however, it’s not that easy to port an OOP code to pure ECS as it uses the HPC# subset (High Performance C#).

Think of HPC# as a viral change requirement. Since it can’t handle managed types, classes can’t be used. The obvious thing to do is to turn a class into a struct, but that’s not enough. Each member variables that is a managed type must be converted to something that HPC# allows. That means that each class referenced inside the converted class must be turned into a struct (or something else). Then you do this recursively to the member variables of those classes and so on.

Most of the time, it’s impossible. If you use any of the classes from .NET or from third party code, you’re stuck. You can’t edit those or it’s not advisable to do so. Just think how many classes or MonoBehaviours do you have that use List or Dictionary. Those code will be tough to convert or never at all.

Ephipany

While I was sharing this post on Reddit, somebody mentioned about copying the Tile information into a separate struct and use that in my ECS A*. I thought that this would be costly since every frame, I have to copy all of the grid’s Tile information to their struct copies. I kept thinking about this, then I realized I don’t need to make copies every frame. I just need to keep and maintain the struct copies.

This means that I should have a separate copy of grid of tiles that are implemented as structs (HPC#). They will only be updated every time the original Tile is changed. I also don’t need to copy every Tile information. I only need to copy what’s needed for pathfinding. The copying also need not to be too synchronized to save CPU. I can defer the copying to multiple frames. Tile changes are not very frequent in the game anyway.

I may have found an effective strategy to somehow port OOP code to pure ECS without a major overhaul. With this in mind, I set out to execute this major change.

Refactoring

This is a partial part of our Tile class:

public sealed class Tile { private readonly TileGrid grid; // The owner grid private readonly GridCell cell; // Contains position, size, etc private NodeHandle<Tile> handle; private readonly Dictionary<Layer, TileItem> itemMap = new Dictionary<string, TileItem>(); private readonly Dictionary<Layer, TileItem> taskItemMap = new Dictionary<string, TileItem>(); private readonly IntVector2 position; private int itemFlags; private Zone zone; ... }

This class is close to 400 lines of code. It handles so many gameplay rules regarding the tiles in the game. It wouldn’t be a good idea to turn this into HPC#. There’s a lot to convert here. At first glance, the usage of Dictionary alone would make the conversion difficult. There’s no HPC# equivalent of it yet. TileItem is a huge data class the contains information about what’s placed in a tile at a certain layer. It contains many member variables that are classes on its own. It’s the same with Zone. If I was thinking of refactoring the Tile for HPC# this way, I wouldn’t do it as it would cascade to a lot more refactoring to other systems. It’s too risky.

The Tile copy that is implemented as a struct looks like this:

public struct TileAsStruct : IComponentData { public int2 position; public int zoneGenderIndex; public byte flags; public bool Contains(byte flag) { return (this.flags & flag) > 0; } // The flags public const byte HAS_PHYSICAL_BLOCKER = 1; public const byte HAS_STUDENT_BLOCKER = 1 << 1; public const byte STAFF_ONLY = 1 << 2; }

Simple, isn’t it? This is all I need for the purposes of pathfinding. It’s implemented as a component so I could easily make queries of it in system classes (the S in ECS). For the actual A* implementation, I used the same implementation as shown in this post. I implemented the custom HeuristicCostCalculator and Reachability that is required for the game’s rules.

As for the system that handles the copying of information from the original tile, I used a separate MonoBehaviour which listens for signals that causes Tile changes. On dispatch of such signals, I just enqueue the positions of the tiles that needs to be recopied. Then per Update(), I copy some X tiles to their struct copies that are taken from the queue.

private const int COPIES_PER_FRAME = 50; private void Update() { int copiedCount = 0; while (this.dirtyTilesQueue.Count > 0 && copiedCount < COPIES_PER_FRAME) { CreateUpdateEntity(); ++copiedCount; } } private void CreateUpdateEntity() { int2 position = this.dirtyTilesQueue.Dequeue(); Tile tile = this.grid.GetTile(position.x, position.y); byte flags = 0; if (tile.HasPhysicalBlocker) { flags |= TileAsStruct.HAS_PHYSICAL_BLOCKER; } if (tile.HasStudentBlocker) { flags |= TileAsStruct.HAS_STUDENT_BLOCKER; } // Check if for staff only if (tile.Zone?.StaffOnly ?? false) { flags |= TileAsStruct.STAFF_ONLY; } TileAsStruct tileValue = new TileAsStruct() { position = position, zoneGenderIndex = tile.Zone?.GenderIndex ?? Gender.ANY.Index, // Use ANY if tile has no zone flags = flags }; // Create the entity that will mark to update the TileAsStruct in the grid Entity updateEntity = this.entityManager.CreateEntity(typeof(UpdateTileAsStruct)); this.entityManager.SetComponentData(updateEntity, new UpdateTileAsStruct(position, tileValue)); }

Just added this image so that the article is not a big wall of text.

Huge Improvements

Our old implementation is so slow that I only allowed up to 4 A* requests per frame. Now with the Burst compiled ECS implementation, I was able to increase it to 40 requests. That’s a 10x improvement! There’s even room for more (it takes around 300 requests to reach 16ms). But pushing the number up is not necessary. The simulation already looks good. Besides, I need to reserve CPU cycles for future features.

That’s it for now. 🙂