Thanks guys for taking the time to answer! I really appreciate your input!
Also, if anyone tried implementing DOTS into an existing 3D MonoBehaviour based project, please let me know how it went. I’d love to know!
This game is going to be a Majesty clone. Right now all I have are NPCs running around attacking enemies in their proximity. There is constant distance checks, action selection, navmesh path selection and more. I’ll go into detail about it below. I would happily go the DOTS road without hesitation but I couldn’t find a tutorial or any info about how to handle NavMesh and animations with SkinnedMeshRenderers. I think these are not covered by DOTS yet?
Though I have the feeling that this answers my question. A bridge between pure ECS and MonoBehaviour seems necessary if you want to convert dynamic, complex games to DOTS.
That’s a wonderful idea, but the systems listed below which would need this are all part of what defines the agent. I just can’t see how to make only part of the system data-driven while maintaining their function and communication with the left over MonoBehaviours.
This is how the system looks in a nutshell (Controllers are all MonoBehaviours):
- AgentController with all System.Action events necessary for all other controllers to subscribe to like OnDrawWeapon, OnAttack, OnDeath, etc.
- AggroSensor to find closest enemy with OverlapSphereNonAlloc
- SkillController to find the skill/spell/action(data classes) with the highest priority, only paused while an action is being performed
Controllers that only listen to AgentController events
- AnimationController
- SFXController
- VFXController
I imagine these last 3 controllers are acceptable MonoBehaviours since they are only waiting for events to fire off.
But controller 2 and 3 are the reason I have the urge to implement DOTS or at least the Job System with Burst. They do a lot every frame.
That’s what I thought too, but I can’t for the life of me figure out how to weave it in to make it do things faster. In the AggroSensor I have implemented a Job to calculate all distances which I thought could be quite the relief for the CPU:
[BurstCompile]
public struct GetDistanceJob : IJob {
public float3 start;
public float3 end;
public NativeArray<float> result;
public void Execute() {
result[0] = math.distance(end, start);
}
}
which I implement inside this method:
public Agent GetClosestEnemy(float range) {
List<Agent> enemies = GetEnemiesInRangeNonAlloc(range); // Peek below
// The typical get closest approach
Agent closest = null;
float closestDist = Mathf.Infinity;
for(int i = 0; i < enemies.Count; i++) {
// Job implementation
NativeArray<float> distanceResult = new NativeArray<float>(1, Allocator.TempJob);
GetDistanceJob job = new GetDistanceJob {
start = _agent.m_transform.position,
end = enemies[i].transform.position,
result = distanceResult
};
JobHandle jobHandle_GetDistance = job.Schedule();
jobHandle_GetDistance.Complete();
float dist = job.result[0];
distanceResult.Dispose();
float currDist = dist;
if(closest == null || currDist < closestDist) {
closest = enemies[i];
closestDist = currDist;
}
}
return closest;
}
List<Agent> GetEnemiesInRangeNonAlloc(float range) {
int numHit = Physics.OverlapSphereNonAlloc(_agent.transform.position, range, _allAgents, 1 << LayerMask.NameToLayer("Agents"));
if(numHit == 0) {
return null;
}
List<Agent> agentList = new List<Agent>();
for(int i = 0; i < numHit; i++) {
Agent agent = _allAgents[i].GetComponent<Agent>();
if(agent != _agent && agent.dead == false && agent.IsEnemy(_agent)) { // ***
agentList.Add(agent);
}
}
return agentList;
}
*** IsEnemy leads to the following relation check and another job implementation:
public static int GetRelations(Faction faction1, Faction faction2) {
// Faction = enum
_relationDataCollection = new NativeArray<RelationData>(data, Allocator.TempJob);
NativeArray<int> relationResult = new NativeArray<int>(1, Allocator.TempJob);
IterateRelationsJob job = new IterateRelationsJob {
relationDataArray = _relationDataCollection,
faction1 = faction1,
faction2 = faction2,
relationResult = relationResult
};
JobHandle jobHandle_GetRelation = job.Schedule();
jobHandle_GetRelation.Complete();
int result = job.relationResult[0];
_relationDataCollection.Dispose();
relationResult.Dispose();
return result;
}
[BurstCompile]
public struct IterateRelationsJob : IJob {
public NativeArray<RelationData> relationDataArray;
public Faction faction1;
public Faction faction2;
public NativeArray<int> relationResult;
public void Execute() {
if(faction1 == faction2) {
relationResult[0] = 256; // If factions are the same, we are friends anyway
}
else {
for(int i = 0; i < relationDataArray.Length; i++) {
RelationData data = relationDataArray[i];
if((data.faction1 == faction1 && data.faction2 == faction2)) {
relationResult[0] = data.relations;
}
}
}
}
}
I was under the impression that no matter where you implemented Jobs, they would always increase performance. But my stress tests with a lot of agents running around had all the same outcomes in the profiler with or without Jobs.
I must be using the Job System incorrectly. Should I rather rewrite the system and iterate over all agents in the scene in one big main Update loop and assign the jobs from there to the controllers (one job / agent)?