You’re thinking along the right lines by moving to a custom data structure. Having written something similar myself in the past, here’s what I would do:
1, Create a dedicated internally-linked Presence class
Less scary than it sounds. It looks like this:
public class Presence
{
// Back-references to useful components for quick access
public GameObject obj;
public Ant ant;
public Pheromone pheromone;
// Internal ring links.
public Presence next;
public Presence prev;
public Presence()
{
// Give ourselves a hug!
next = this;
prev = this;
// Note: prev and next are ALWAYS valid, never null.
// That saves a whole lot of null checks!
// Manipulating rings is branchless, unlike linked lists.
}
public void Disconnect()
{
next.prev = prev;
prev.next = next;
next = this;
prev = this;
}
public void InsertAfter(Presence other)
{
// Disconnect from where we are...
next.prev = prev;
prev.next = next;
// ...and insert in our new location
next = other.next;
prev = other;
next.prev = this;
prev.next = this;
}
}
This Presence class serves as a handy reference for all the different types of component there might be on an object in your game world. If you like, you can add a ‘type’ enum for quick identification and filtering.
Why is the class internally linked? Because it makes updating a world map a breeze:
2. Create a PresenceMap singleton
This is also pretty straightforward:
public class PresenceMap : MonoBehaviour
{
public static PresenceMap inst;
public Vector2 minBound;
public Vector2 maxBound;
public float resolution; // Set this to the range at which you're interested in sensing objects.
Presence[,] map;
int maxCellX;
int maxCellY;
List<Presence> neighbours = new List<Presence>();
private void Awake()
{
inst = this;
int dimx = (int)((maxBound.x - minBound.x) / resolution);
int dimy = (int)((maxBound.y - minBound.y) / resolution);
map = new Presence[dimx, dimy];
maxCellX = dimx - 1;
maxCellY = dimy - 1;
// Seed the array with blank roots to make relocation of presences easy.
for (int x = 0; x < dimx; ++x)
{
for (int y = 0; y < dimy; ++y)
{
map[x, y] = new Presence();
}
}
}
//Call when you create or move a presence to keep its location up to date.
// Note: There's no need to inform the PresenceMap when you remove or destroy
// a Presence. Just call Disconnect on the Presence.
public void UpdatePresence(Presence p)
{
Vector2 presencePos = p.trans.position;
Vector2 mapPos = (presencePos - minBound) / resolution;
int cellX = Mathf.Clamp((int)mapPos.x, 0, maxCellX);
int cellY = Mathf.Clamp((int)mapPos.y, 0, maxCellY);
p.InsertAfter(map[cellX, cellY]);
}
//Build a list of neighbours based on the nearest four cells
//NOTE: Re-uses a single list to avoid garbage. DO NOT RETAIN RESULT!
public List<Presence> GetNeighbours(Presence p)
{
neighbours.Clear();
// Assumes the x/y plane is the relevant one; adapt accordingly.
Vector2 presencePos = p.trans.position;
Vector2 mapPos = (presencePos - minBound) / resolution;
int cellX = Mathf.Clamp((int)(mapPos.x + 0.5f), 1, maxCellX);
int cellY = Mathf.Clamp((int)(mapPos.y + 0.5f), 1, maxCellY);
AddNeighbours(map[cellX - 1, cellY - 1]);
AddNeighbours(map[cellX, cellY - 1]);
AddNeighbours(map[cellX - 1, cellY]);
AddNeighbours(map[cellX, cellY]);
return neighbours;
}
void AddNeighbours(Presence root)
{
Presence iter = root.next;
while (iter != root)
{
neighbours.Add(iter);
iter = iter.next;
}
}
}
3. Profit!
Anything you want to have a presence in the game, give it a Presence!
Hook up the trans/Ant/Pheromone back-references as appropriate.
Call PresenceMap.inst.UpdatePresence(myPresence) when you add or move the presence (so for static stuff you can just add it once and leave it).
Call myPresence.Disconnect() to remove it from the map (eg when the object it wraps is destroyed)
To check your surroundings, just call GetNeighbours and do a quick square-distance check on each if you want more accuracy (or add a variation of GetNeighbours that takes a range and does that for you during AddNeighbour)
If you need to gather neighbours in a radius bigger than the resolution of the map, it’s not hard to add a version of GetNeighbours that takes a range and gathers from a square of cells around the specified Presence.
Once you have your neighbours, you can either use a type enum or check which of the back-references (to Ant or Pheromone) are non-null, and off you go. No GetComponent, no physics overlap, no garbage generated.
You can make Presence a monobehaviour in its own right, but that means spawning the big 2D array of roots actually in the scene. On the positive side, it gives you something you can inspect at runtime, and you can always tuck the root array away under a deactivated gameobject so that Unity doesn’t process them. It’s up to you. If you don’t make Presence a monobehaviour, your Ant and Pheromone classes can just create and hold onto one themselves, and you’ll save some processing time on the static ones.
If you give Presence its own Vector2 position, you can remove its dependence on Transform and allow non-monobehaviour classes to be present in the map too. Whether that’s useful is up to you.
You should find this makes a pretty big difference to the speed of your interaction-checking. You can probably remove colliders/triggers entirely from stuff like pheromones. While Unity’s physics are indeed very fast and efficient, manual overlap checks almost certainly bypass important optimisations, especially those that rely on frame-to-frame coherency. A bucketing system like this one, tailored to the range of influence that you need and involving no garbage collection, no scene-gathering, no interrogation of collider properties - it’s going to be at least two, more likely three orders of magnitude quicker, and that’s without considering the saving from GetComponent.
If different objects have greatly different radii of influence:
Consider supporting multiple layers of map within the PresenceManager, each with a different resolution, and having UpdatePresence() take a parameter to indicate which layer the presence should be pushed into. Then GetNeighbours can work through each layer internally to return an overall list of neighbours. Layers with a smaller resolution will return neighbours from a smaller area, and layers with a larger resolution from a larger area, so you don’t end up pulling in lots and lots of unwanted short-range results just because you need to check for longer-range ones.
Good luck!
NB: These classes ought to compile as-is (bar the Ant and Pheromone stuff) but I wrote them from scratch for this post so they might be (dons sunglasses) buggy.