0
\$\begingroup\$

Preface I realized that the root of my question is a little simpler than all of the details below make it out to be. All other things being equal, what is the most important system aspect to optimize for in order to prevent performance impacts from showing up later? Should I be reducing the main thread load, reducing the overall CPU load across all threads, or reducing the game's memory footprint? It seems like a simple question, but I haven't seen it addressed anywhere.

All the glorious specifics:

I am creating a 2D top-down game with custom mesh tilemaps. I have a fixed size map, but I'm chunking anyways. I have an asynchronous worker thread that will generate a mesh data object that will be imported into the chunk:

public class MeshData
{
    public List<Vector3> verticies = new List<Vector3>();
    public List<int> triangles = new List<int>();
    public List<Vector2> uv = new List<Vector2>();
}

I can run my noise sampling and triangle creation in the worker thread and then import the mesh data via the main thread when the data is ready. This works for world generation, but I'm not sure what to do for on-the-fly updates.

Specifically, I need to change the Z coordinate of any mesh point at any time when the player raises/lowers the terrain. My first thought was to directly edit the MeshFilter from the worker thread, but Unity doesn't allow secondary threads to modify gameobjects. So, that leaves me with a few other options:

  1. Save a copy of MeshData in each chunk and modify that in the worker thread. Re-import in the main thread when the work is done. (This consumes 208Mb overall if my map size doesn't change)
  2. Re-generate the mesh triangles from the saved coordinate array. (burns unnecessary CPU cycles on secondary thread, but no additional memory usage).
  3. Edit the mesh directly from the game thread. (Puts all the work on the main thread, probably a bad idea)
  4. Implement coroutines (Still puts the work on the main thread, but more "gracefully" (source))
  5. I need to re-write my chunk generation entirely, I obviously failed to learn elementary idea "X".

Since I am an embedded developer by day, I often find myself worrying about clock cycles and memory allocations, perhaps more than I need to. To be clear, I don't have a lag problem yet, I'm just doing my best to avoid creating one. From what I can see, lag is a common problem around here. (ex. 1, ex. 2, ex. 3) I am capable of doing any of the above (or something else I missed), I just don't know which is the most viable solution. Any expert opinions would be gladly appreciated.

Excerpt from my chunk generator

public class ChunkGenerator : MonoBehaviour
{
    private MeshFilter filter;

    // Can be called in async thread.    
    public void GenerateHeightMap(Vector2Int chunkPos, Vector2Int chunkSize, Vector2Int worldSize, WorldPoint[,] grid)
    {
        // Set the real coordinates for this grid point and calculate the height
    }

    // Can be called in async thread.    
    public void CreateChunk(Vector2Int chunkPos, Vector2Int chunkSize, WorldPoint[,] grid)
    {
        MeshData mesh = new MeshData();
        // Some for loops here to iterate over the cells.
                MeshUtilities.CreateCell(mesh, cellPoints);
        this.meshData = mesh;
    }

    // Called by main thread, interacts with a filter.
    public void LoadMeshData()
    {
        filter = GetComponent<MeshFilter>();
        Mesh mesh = new Mesh()
        {
            vertices = this.meshData.verticies.ToArray(),
            uv = this.meshData.uv.ToArray(),
            triangles = this.meshData.triangles.ToArray()
        };
        filter.mesh = mesh;
        filter.mesh.RecalculateNormals(); // Enables shadows.
    }

Excerpt from my world generator thread

private void ChunkManagementThread()
{
    // While loop allows us to bail if the main thread dies (game exit).
    while (chunkGenerationThreadRunning)
    {
        for (int x = 0; x < chunkArraySize; x++) for (int y = 0; y < chunkArraySize; y++)
        {
            ChunkGenerator chunk = this.chunks[x,y];

            lock(chunk)
            {
                switch (chunk.state)
                {
                    case ChunkState.GenerateHeightmap:
                        // Pass 1: Set the height for all points.
                        this.chunks[x, y].GenerateHeightMap(new Vector2Int(x,y),this.realChunkSize,this.realWorldSize,this.grid);
                    break;
                    case ChunkState.CreateChunk:
                        this.chunks[x, y].CreateChunk(new Vector2Int(x, y), this.realChunkSize, this.grid);
                        // Pass 3: Create triangles.
                    break;
                    case ChunkState.ActivateMesh:
                        // Finally, render the image. (Actually from main thread, not shown)
                        this.chunks[x, y].LoadMeshData(this.chunks[x, y].GetMesh());
                    break;
                    case ChunkState.UpdatesPending:
                        // Need to process updates here.
                    break;
                        // Etc...

(Note: I did cut my code down a lot since my problem isn't with syntax. It probably won't run as shown; I can post the full version if needed)

\$\endgroup\$
  • 2
    \$\begingroup\$ What about profiling your application for a first ? \$\endgroup\$ – Aybe Feb 26 at 22:34
  • 1
    \$\begingroup\$ Your initial question is based on just one factor, "Where does my bottleneck lie?" Only you can know that, by profiling your code (now and later as features develop and bottlenecks possibly change). \$\endgroup\$ – Engineer Feb 27 at 7:01
2
\$\begingroup\$

As your initial question is based on just one factor - "Where does my bottleneck lie?" - only you can get at the answers, by profiling your code (now and later as features develop and bottlenecks change). Sounds to me as if you want us to project against future changes, which we cannot possibly do, given how your requirements set may change in future.

Even if we said right now that "reducing the overall CPU load across all threads" was the answer, some other subsystem may be implemented by you later that changes the dynamic and actually requires more main thread time, in which case you'd have been better off with "reducing the main thread load". That is how these things inevitably work. You cannot be certain until you get to point B. You can project; and if that's hard for you to do, it's impossible for us, knowing nothing about where your spec is at or is headed, especially (given your questions) your desired threading model.

Heavy optimisation makes code brittle and difficult to change; simpler, earlier "root" solutions to the problem (what we often call the naive solutions) are a better place to start (re)factoring from.

Either stick with simpler, more refactorable optimisations for now, or provide multiple optimisation approaches and maintain unit tests for these, by developing them against a single interface, thus requiring only one set of tests for all variations. By testing them against one another, you can find answers to your questions, but it will take work, and time.

One problem here is that method calls themselves have overheads, so interfaces may cost you; but this is the price you'd have to pay for higher flexibility.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.