Scaling the play area up with object pooling

The impementation in the lesson is lovely and educational and simple, but it doesn’t scale very well. Instantiating an object for every single grid cell in a play area doesn’t let you grow the play area much before it really becomes a hindrance on startup. So, I started using object pooling, in which rather than calling Instantiate() and creating a new game object in the scene for every grid cell, I’m creating a couple of hundred and reusing them as needed when the player selects the move action.

My startups are now a lot faster, and overall memory use is down by a decent about. I set up the Synty Polygon Battle Royale demo scene with a 160x280 grid of 1x1 meter GridPositions, which would otherwise be almost 45,000 objects, and my startups were lagging by most of 20 seconds, but this pooling method is performing quite well with only a pool of 500. See the following class:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Pool;

public class GridSystemVisual : MonoBehaviour
{
  public static GridSystemVisual Instance { get; private set; }

  private LinkedPool<GridSystemVisualSingle> gridVisualSinglePool;
  private Dictionary<GridPosition, GridSystemVisualSingle> gridVisualSingleDictionary = new Dictionary<GridPosition, GridSystemVisualSingle>();

  [Serializable]
  public struct GridVisualTypeMaterial
  {
    public GridVisualType gridVisualType;
    public Material material;
  }

  public enum GridVisualType { White, Blue, Red, RedSoft, Yellow }

  [SerializeField] private Transform gridSystemVisualSinglePrefab;
  [SerializeField] private List<GridVisualTypeMaterial> gridVisualTypeMaterials;

  #region Unity lifecycle events
  private void Awake()
  {
    if (Instance == null)
    {
      Instance = this;
    }
    else
    {
      Debug.LogError($"More than one GridSystemVisual created. Destroying this one.");
      Destroy(this.gameObject);
    }

    this.gridVisualSinglePool = new LinkedPool<GridSystemVisualSingle>(
      () =>
      {
        Transform singleGrid = Instantiate(this.gridSystemVisualSinglePrefab, Vector3.zero, Quaternion.identity);
        return singleGrid.GetComponent<GridSystemVisualSingle>();
      },
      (gridVisualSingle) => gridVisualSingle.gameObject.SetActive(true),
      (gridVisualSingle) => gridVisualSingle.gameObject.SetActive(false),
      (gridVisualSingle) => Destroy(gridVisualSingle),
      false, 500);
  }

  private void Start()
  {
    UnitActionSystem.Instance.OnSelectedActionChanged += OnSelectedActionChanged;
    LevelGrid.Instance.OnAnyUnitMovedGridPosition += OnAnyUnitMovedGridPosition;
    BaseAction.OnAnyActionStarted += OnAnyActionStarted;
    BaseAction.OnAnyActionCompleted += OnAnyActionCompleted;
    this.UpdateGridVisual();
  }
  #endregion

  public void HideAllGridPositions()
  {
    foreach (var visual in this.gridVisualSingleDictionary)
    {
      visual.Value.Hide();
      this.gridVisualSinglePool.Release(visual.Value);
    }

    this.gridVisualSingleDictionary.Clear();
  }

  public void ShowGridPositionList(List<GridPosition> positions, GridVisualType gridVisualType)
  {
    if (positions != null)
    {
      foreach (GridPosition position in positions)
      {
        GridSystemVisualSingle single;

        if (this.gridVisualSingleDictionary.ContainsKey(position))
        {
          single = this.gridVisualSingleDictionary[position];
        }
        else
        {
          single = this.gridVisualSinglePool.Get();
          this.gridVisualSingleDictionary[position] = single;
        }

        single.transform.position = LevelGrid.Instance.GetWorldPosition(position);
        single.transform.rotation = Quaternion.identity;
        single.Show(this.GetGridVisualTypeMaterial(gridVisualType));
      }
    }
  }

  private void ShowGridPositionRange(GridPosition gridPosition, int range, GridVisualType gridVisualType)
  {
    List<GridPosition> gridPositions = new List<GridPosition>();

    for (int x = -range; x <= range; x++)
    {
      for (int z = -range; z <= range; z++)
      {
        GridPosition actual = gridPosition + new GridPosition(x, z);

        if (LevelGrid.Instance.IsValidGridPosition(actual))
        {
          if (GridSystem<GridObject>.DistanceBetweenGridPositions(gridPosition, actual) <= range)
          {
            gridPositions.Add(actual);
          }
        }
      }
    }

    this.ShowGridPositionList(gridPositions, gridVisualType);
  }

  private void ShowGridPositionRangeSquare(GridPosition gridPosition, int range, GridVisualType gridVisualType)
  {
    List<GridPosition> gridPositions = new List<GridPosition>();

    for (int x = -range; x <= range; x++)
    {
      for (int z = -range; z <= range; z++)
      {
        GridPosition actual = gridPosition + new GridPosition(x, z);

        if (LevelGrid.Instance.IsValidGridPosition(actual))
        {
          gridPositions.Add(actual);
        }
      }
    }

    this.ShowGridPositionList(gridPositions, gridVisualType);
  }

  public void UpdateGridVisual()
  {
    this.HideAllGridPositions();

    Unit selectedUnit = UnitActionSystem.Instance.SelectedUnit;
    BaseAction selectedAction = UnitActionSystem.Instance.SelectedAction;

    GridVisualType gridVisualType = GridVisualType.White;

    switch (selectedAction)
    {
      case MoveAction moveAction:
        gridVisualType = GridVisualType.White;
        break;
      case SpinAction spinAction:
        gridVisualType = GridVisualType.Blue;
        break;
      case ShootAction shootAction:
        gridVisualType = GridVisualType.Red;
        this.ShowGridPositionRange(selectedUnit.CurrentGridPosition, shootAction.MaxShootDistance, GridVisualType.RedSoft);
        break;
      case GrenadeAction grenadeAction:
        gridVisualType = GridVisualType.Yellow;
        break;
      case SwordAction swordAction:
        gridVisualType = GridVisualType.Red;
        this.ShowGridPositionRangeSquare(selectedUnit.CurrentGridPosition, swordAction.MaxSwordDistance, GridVisualType.RedSoft);
        break;
      case InteractAction interactAction:
        gridVisualType = GridVisualType.Blue;
        break;
    }

    this.ShowGridPositionList(selectedAction.GetValidActionGridPositions(), gridVisualType);
  }

  private void OnSelectedActionChanged(object sender, EventArgs e)
  {
    this.UpdateGridVisual();
  }

  private void OnAnyUnitMovedGridPosition(object sender, EventArgs e)
  {
    // I'm now hiding the visuals as the unit moves and re-showing them after it stops at the destination
    // this.UpdateGridVisual();
  }

  private void OnAnyActionStarted(object sender, EventArgs e)
  {
    this.HideAllGridPositions();
  }

  private void OnAnyActionCompleted(object sender, EventArgs e)
  {
    this.UpdateGridVisual();
  }

  private Material GetGridVisualTypeMaterial(GridVisualType gridVisualType)
  {
    try
    {
      return this.gridVisualTypeMaterials.First(tm => tm.gridVisualType == gridVisualType).material;
    }
    catch (Exception)
    {
      Debug.LogError($"GridSystemVisual: Couldn't locate material for type {gridVisualType}.");
    }

    return null;
  }
}

I’m using the LinkedPool to pool the objects:
private LinkedPool<GridSystemVisualSingle> gridVisualSinglePool;

and when they’re in the scene, rather than using a 2D array of all grid cells, I’m using a Dictionary to map GridPositions to GridSystemVisualSingles:
private Dictionary<GridPosition, GridSystemVisualSingle> gridVisualSingleDictionary = new Dictionary<GridPosition, GridSystemVisualSingle>();

Then, on hide, I can just loop the dictionary, hide, and release the object back to the pool:

  public void HideAllGridPositions()
  {
    foreach (var visual in this.gridVisualSingleDictionary)
    {
      visual.Value.Hide();
      this.gridVisualSinglePool.Release(visual.Value);
    }

    this.gridVisualSingleDictionary.Clear();
  }

and on show, I can grab from the pool, stuff it in the dictionary under the grid cell where it’ll live, and set its position, rotation, and material:

  public void ShowGridPositionList(List<GridPosition> positions, GridVisualType gridVisualType)
  {
    if (positions != null)
    {
      foreach (GridPosition position in positions)
      {
        GridSystemVisualSingle single;

        if (this.gridVisualSingleDictionary.ContainsKey(position))
        {
          single = this.gridVisualSingleDictionary[position];
        }
        else
        {
          single = this.gridVisualSinglePool.Get();
          this.gridVisualSingleDictionary[position] = single;
        }

        single.transform.position = LevelGrid.Instance.GetWorldPosition(position);
        single.transform.rotation = Quaternion.identity;
        single.Show(this.GetGridVisualTypeMaterial(gridVisualType));
      }
    }
  }
4 Likes

An excellent optimization.

Yeah if you want to massively increase the size of the grid then a different approach is necessary, object pooling is a great approach.
Another great one is to put it all in one single mesh, it’s more complex but insanely performant Code Monkey - Custom Tilemap in Unity with Saving and Loading (Level Editor)
Code Monkey - Make Awesome Effects with Meshes in Unity | How to make a Mesh

2 Likes

Privacy & Terms