So this thread is pretty much solved. Here’s my CombatantManager now, now renamed to CombatantSpatialPartition, and is significantly longer than before, but provides much faster performance:
using System.Collections.Generic;
using System.Linq;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
public class CombatantSpatialPartition : MonoBehaviour {
public static CombatantSpatialPartition Instance;
public int gridSize = 10;
public float cellSize = 10f;
public List<Combatant> combatantsInScene;
public List<CombatantInfo>[] grid;
float3 gridPosition;
public static int FloorToInt (float f) => f >= 0 ? (int) f : ((int) f == f ? (int) f : (int) f - 1);
public static int CeilToInt (float f) => f < 0 ? (int) f : ((int) f + ((f - (int) f != 0) ? 1 : 0));
public static int count;
void Awake () {
if (Instance != null) {
Destroy (gameObject);
}
Instance = this;
combatantsInScene = new List<Combatant> ();
// Initialize the grids dictionary for each team
grid = new List<CombatantInfo>[gridSize * gridSize * 2];
for (int i = 0; i < gridSize * gridSize * 2; i++) {
grid[i] = new List<CombatantInfo> ();
}
}
void Update () {
gridPosition = transform.position;
ManageCombatants ();
}
bool equals (int2 x, int2 y) {
return x.x == y.x && x.y == y.y;
}
public void AddCombatant (CombatantInfo combatantInfo) {
int2 cell = WorldToGrid (combatantInfo.position);
int linearIndex = combatantInfo.teamID * (gridSize * gridSize) + cell.x * gridSize + cell.y;
grid[linearIndex].Add (combatantInfo);
count++;
}
public void UpdateCombatant (CombatantInfo combatantInfo, float3 oldPosition) {
int2 oldCell = WorldToGrid (oldPosition);
RemoveCombatant (combatantInfo, oldCell);
AddCombatant (combatantInfo);
}
public void RemoveCombatant (CombatantInfo combatantInfo, int2 cell) {
int linearIndex = combatantInfo.teamID * (gridSize * gridSize) + cell.x * gridSize + cell.y;
grid[linearIndex].Remove (combatantInfo);
count--;
}
public void RemoveCombatantDeep (CombatantInfo combatantInfo) {
for (int x = 0; x < gridSize; x++) {
for (int y = 0; y < gridSize; y++) {
int linearIndex = combatantInfo.teamID * (gridSize * gridSize) + x * gridSize + y;
if (grid[linearIndex].Contains (combatantInfo)) {
grid[linearIndex].Remove (combatantInfo);
count--;
}
}
}
}
public void ManageCombatants () {
int count = combatantsInScene.Count;
// Perform a single sphere cast using SpherecastCommand and wait for it to complete
// Set up the command and result buffers
var results = new NativeArray<RaycastHit> (combatantsInScene.Count, Allocator.TempJob);
var commands = new NativeArray<SpherecastCommand> (combatantsInScene.Count, Allocator.TempJob);
for (int i = 0; i < count; i++) {
Combatant combatant = combatantsInScene[i];
CombatantInfo info = combatant.GetInfo ();
//combatant.nearbyCombatants = ManageNearbyCells (info);
NativeList<int2> nearbyCells = new NativeList<int2> (Allocator.TempJob);
GetNearbyCellsJob job = new GetNearbyCellsJob {
combatantInfo = info,
gridSize = gridSize,
cellSize = cellSize,
gridPosition = gridPosition,
nearbyCells = nearbyCells
};
JobHandle jobHandle = job.Schedule ();
jobHandle.Complete ();
List<int2> nearbyCellsList = new List<int2> ();
for (int j = 0; j < nearbyCells.Length; j++) {
nearbyCellsList.Add (nearbyCells[j]);
}
nearbyCells.Dispose ();
combatant.nearbyCells = nearbyCellsList;
if (!combatant.seekingEnemy) {
continue;
}
// Set the data of the first command
Vector3 origin = combatant.position;
Vector3 direction = (combatant.closestEnemyPosition - (Vector3) combatant.position).normalized;
float radius = combatant.spherecastRadius;
QueryParameters queryParameters = new QueryParameters {
layerMask =combatant.detectionMask,
};
commands[i] = new SpherecastCommand (origin, radius, direction, queryParameters, Vector3.Distance (combatant.position, combatant.closestEnemyPosition)) {
queryParameters = queryParameters,
};
}
// Schedule the batch of sphere casts
var sphereCastHandle = SpherecastCommand.ScheduleBatch (commands, results, count / SystemInfo.processorCount, default (JobHandle));
// Wait for the batch processing job to complete
sphereCastHandle.Complete ();
for (int i = 0; i < count; i++) {
combatantsInScene[i].lastSphereCastHit = results[i];
}
// Dispose the buffers
results.Dispose ();
commands.Dispose ();
}
public float3 GridToWorld (int2 gridPosition) {
float3 min = this.gridPosition - new float3 (gridSize * cellSize, 0, gridSize * cellSize) * 0.5f;
return new float3 (min.x + gridPosition.x * cellSize + cellSize / 2, 0, min.z + gridPosition.y * cellSize + cellSize / 2);
}
public int2 WorldToGrid (float3 pos) {
float3 min = gridPosition - new float3 (gridSize * cellSize, 0, gridSize * cellSize) * 0.5f;
int2 gridCoordinates = new int2 (
Mathf.FloorToInt ((pos.x - min.x) / cellSize),
Mathf.FloorToInt ((pos.z - min.z) / cellSize)
);
// Clamp the grid coordinates within the grid bounds
gridCoordinates.x = Mathf.Clamp (gridCoordinates.x, 0, gridSize - 1);
gridCoordinates.y = Mathf.Clamp (gridCoordinates.y, 0, gridSize - 1);
return gridCoordinates;
}
[BurstCompile]
public struct GetNearbyCellsJob : IJob {
[ReadOnly] public CombatantInfo combatantInfo;
[ReadOnly] public int gridSize;
[ReadOnly] public float cellSize;
[ReadOnly] public float3 gridPosition;
[WriteOnly] public NativeList<int2> nearbyCells;
public void Execute () {
CombatantInfo combatant = combatantInfo;
float radius = combatant.combatantCheckRadius;
float3 pos = combatant.position;
int2 centerCell = WorldToGrid (pos);
int combatantTeamID = combatant.teamID;
int enemyTeamID = combatantTeamID == 0 ? 1 : 0;
int cellsToCheck = CeilToInt (radius / cellSize);
int2 minCell = new int2 (math.max (0, centerCell.x - cellsToCheck), math.max (0, centerCell.y - cellsToCheck));
int2 maxCell = new int2 (math.min (gridSize - 1, centerCell.x + cellsToCheck), math.min (gridSize - 1, centerCell.y + cellsToCheck));
int totalCells = (maxCell.x - minCell.x + 1) * (maxCell.y - minCell.y + 1);
for (int i = 0; i < totalCells; i++) {
int x = minCell.x + (i % (maxCell.x - minCell.x + 1));
int z = minCell.y + (i / (maxCell.x - minCell.x + 1));
int2 cell = new int2 (x, z);
// Check if the cell is within the grid boundaries
if (x >= 0 && x < gridSize && z >= 0 && z < gridSize) {
// Calculate the cell's center position in world coordinates
float3 cellCenter = GridToWorld (cell);
// Check if the cell is within the combatant's radius
if (math.distance (pos, cellCenter) <= radius + math.sqrt (2 * cellSize * cellSize) / 2) {
int linearIndex = enemyTeamID * (gridSize * gridSize) + x * gridSize + z;
nearbyCells.Add (cell);
}
}
}
}
public float3 GridToWorld (int2 gridPosition) {
float3 min = this.gridPosition - new float3 (gridSize * cellSize, 0, gridSize * cellSize) * 0.5f;
return new float3 (min.x + gridPosition.x * cellSize + cellSize / 2, 0, min.z + gridPosition.y * cellSize + cellSize / 2);
}
public int2 WorldToGrid (float3 pos) {
float3 min = gridPosition - new float3 (gridSize * cellSize, 0, gridSize * cellSize) * 0.5f;
int2 gridCoordinates = new int2 (
Mathf.FloorToInt ((pos.x - min.x) / cellSize),
Mathf.FloorToInt ((pos.z - min.z) / cellSize)
);
// Clamp the grid coordinates within the grid bounds
gridCoordinates.x = Mathf.Clamp (gridCoordinates.x, 0, gridSize - 1);
gridCoordinates.y = Mathf.Clamp (gridCoordinates.y, 0, gridSize - 1);
return gridCoordinates;
}
}
void OnDrawGizmos () {
if (!Application.isPlaying) {
return;
}
Vector3 min = transform.position - new Vector3 (gridSize * cellSize, 0, gridSize * cellSize) * 0.5f;
for (int x = 0; x < gridSize; x++) {
for (int z = 0; z < gridSize; z++) {
int2 cellCoord = new int2 (x, z);
Vector3 cellCenter = new Vector3 (min.x + x * cellSize + cellSize / 2, 0, min.z + z * cellSize + cellSize / 2);
int combatantCount = 0;
if (grid != null) {
for (int i = 0; i < 2; i++) {
int linearIndex = i * (gridSize * gridSize) + x * gridSize + z;
foreach (var teamGrids in grid[linearIndex]) {
combatantCount++;
}
}
}
// Fill cell with color based on combatant count
Gizmos.color = combatantCount > 0 ? Color.green : Color.white;
Gizmos.DrawWireCube (cellCenter, new Vector3 (cellSize, 0.1f, cellSize));
// Draw combatant count label on cell
GUIStyle style = new GUIStyle ();
style.normal.textColor = Color.black;
style.alignment = TextAnchor.MiddleCenter;
style.fontSize = 14;
#if UNITY_EDITOR
Handles.Label (cellCenter, combatantCount.ToString (), style);
#endif
}
}
Gizmos.color = Color.white;
}
}
Note: Some comments are artifacts from previous script versions