Hi all,
Thanks to this forum and a few tutorials around the place I have finally been able to make the beginning of a game in ECS. I am starting to be able to self correct my mistakes and trying a few ideas but what I think would be really helpful is pulling up the covers and getting some feedback on a couple of my systems and approach.
- I have x amount of locations (currently 50). They are just ‘areas’ 50 in the x and z.
- in those locations I spawn (currently) 100 entities
- These entities are basic cubes, no physics. They have 1 component (will split next code changes) called
HumanData
- Will probably split Infection stuff to
InfectionData
- Conceptually for the game, they can either be infected or uninfected. true or false. Keep it simple for now.
public struct HumanData : IComponentData {
public int Location; // simple int will correspond to a location
public float3 OffsetPosition; // offset for position
public float3 StartPos;
public float3 EndPos;
public float3 TravelVector;
public float Distance;
public float Speed;
public int MyIndex;
// infection stuff
public bool isInfected;
public bool isAsymptomatic;
}
- I randomise movement so they can move around to different points in there locations (HumanMoveSystem)
- They cannot currently move outside that initial location area. There new ‘endPos’ is offset by the locations position so they always stay in that area.
[UpdateAfter(typeof(RandomSystem))]
public class HumanMoveSystem : SystemBase
{
protected override void OnUpdate()
{
float deltaTime = Time.DeltaTime;
var randomArray = World.GetExistingSystem<RandomSystem>().RandomArray;
float locationSize = (GameDataManager.instance.locationSize - 1) / 2.0f;
Entities
.WithNativeDisableParallelForRestriction(randomArray)
.WithName("HumanMoveSystem")
.WithBurst()
.ForEach((int nativeThreadIndex, ref Translation position, ref Rotation rotation, ref HumanData humanData) =>
{
float3 travelVector = humanData.EndPos - humanData.StartPos;
position.Value += math.normalize(travelVector) * deltaTime * humanData.Speed;
humanData.TravelVector = math.normalize(travelVector) * deltaTime * humanData.Speed;
float distance = math.distance(position.Value, humanData.EndPos);
humanData.Distance = distance;
if (distance < 1f)
{
humanData.StartPos = humanData.EndPos;
float distanceCheck;
do
{
var random = randomArray[nativeThreadIndex];
float x = random.NextFloat(-1 * locationSize, locationSize);
float z = random.NextFloat(-1 * locationSize, locationSize);
humanData.EndPos = new float3(x, 0, z) + humanData.OffsetPosition;
distanceCheck = math.distance(humanData.StartPos, humanData.EndPos);
randomArray[nativeThreadIndex] = random;
}
while (distanceCheck < locationSize/2.0f);
}
})
.Schedule();
}
}
- Because of this, I have a patient zero in each location (for now, while I work on improvements)
- as an entity A distance to another entity B reaches close enough (distance < 0.1f) - if A or B are infected, then both become infected.(HumanInfectionDetectionSystem)
[UpdateAfter(typeof(HumanMoveSystem))]
public class HumanInfectionDetectionSystem : JobComponentSystem
{
EntityQuery m_Group;
protected override void OnCreate()
{
var query = new EntityQueryDesc()
{
All = new ComponentType[]
{
typeof(HumanData),
ComponentType.ReadOnly<Translation>()
}
};
m_Group = GetEntityQuery(query);
}
[BurstCompile]
public struct DistanceCheckJob : IJobParallelFor
{
[NativeDisableParallelForRestriction] public NativeArray<HumanData> HumanData;
[ReadOnly] public NativeArray<Translation> TranslationData;
public void Execute(int index)
{
var currentData = HumanData[index];
var currentPos = TranslationData[index];
for (var i = index + 1; i < HumanData.Length; i++)
{
var humanBPosition = TranslationData[i];
var humanBData = HumanData[i];
if (currentData.Location == humanBData.Location)
{
// if we are already infected, ignore
if ((!currentData.isInfected && humanBData.isInfected) ||
(currentData.isInfected && !humanBData.isInfected))
{
if (math.distance(currentPos.Value, humanBPosition.Value) < 0.5f)
{
currentData.isInfected = true;
humanBData.isInfected = true;
HumanData[index] = currentData;
HumanData[i] = humanBData;
}
}
}
}
}
}
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var humanData = m_Group.ToComponentDataArray<HumanData>(Allocator.TempJob);
var translationData = m_Group.ToComponentDataArray<Translation>(Allocator.TempJob);
var distanceCheckJob = new DistanceCheckJob
{
HumanData = humanData,
TranslationData = translationData,
};
var collisionJobHandle = distanceCheckJob.Schedule(translationData.Length, 32);
collisionJobHandle.Complete();
// ToComponentDataArray copies the data, so we need to write it back again
m_Group.CopyFromComponentDataArray(humanData);
humanData.Dispose();
translationData.Dispose();
return collisionJobHandle;
}
}
- If an entity is infected, I change the material on it. This is a bottleneck system due to needing to use EntityManager on a SharedComponent (ShowInfectionMaterialSystem)
[UpdateAfter(typeof(HumanMoveSystem))]
public class ShowInfectionMaterialSystem : SystemBase
{
Material newMaterial;
// this just checks to see if the object is infected, if so, change the material
protected override void OnUpdate()
{
if (newMaterial == null)
newMaterial = GameDataManager.instance.infectedMaterial;
Entities
.WithName("ShowInfectionMaterialSystem")
.WithoutBurst()
.WithStructuralChanges()
.ForEach((Entity entity, ref HumanData infectionData) =>
{
if (infectionData.isInfected && !infectionData.isAsymptomatic)
{
var render = EntityManager.GetSharedComponentData<RenderMesh>(entity);
EntityManager.SetSharedComponentData(entity, new RenderMesh() { mesh = render.mesh, material = newMaterial });
infectionData.isAsymptomatic = true;
}
})
.Run();
}
}
- Finally just for visual purposes, over each location I have a text field I update with the ‘% infected’ (InformationSystem)
[UpdateAfter(typeof(HumanInfectionDetectionSystem))]
public class InformationSystem : JobComponentSystem
{
EntityQuery m_Group;
protected override void OnCreate()
{
var query = new EntityQueryDesc()
{
All = new ComponentType[]
{
ComponentType.ReadOnly<HumanData>()
}
};
m_Group = GetEntityQuery(query);
}
[BurstCompile]
public struct PercentageUpdateJob : IJobFor
{
[ReadOnly] public NativeArray<HumanData> HumanData;
public NativeArray<int> Totals;
public void Execute(int index)
{
var currentData = HumanData[index];
if (currentData.isInfected && currentData.isAsymptomatic)
Totals[currentData.Location]++;
}
}
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var humanData = m_Group.ToComponentDataArray<HumanData>(Allocator.TempJob);
NativeArray<int> totals = new NativeArray<int>(GameDataManager.instance.Locations.Length, Allocator.TempJob);
var percentageUpdateJob = new PercentageUpdateJob
{
HumanData = humanData,
Totals = totals
};
var collisionJobHandle = percentageUpdateJob.Schedule(humanData.Length, inputDependencies);
collisionJobHandle.Complete();
InfoManager.instance.PercentageTotals = totals.ToArray();
totals.Dispose();
humanData.Dispose();
return collisionJobHandle;
}
}
- I have messed around with chunks, but was unsure how I could chunk some of the ideas going on here.
- I think my distance check is actually expensive (have not measured). Would an AABB type check be better, that is just checking my position values?
- The bottleneck is the
ShowInfectionMaterialSystem
. How can I get an improvement from this idea? - Since locations is set at the start, I was hoping to perhaps set my native int array ‘totals’, in the
InformationSystem
once, rather than every job, as it will never grow or shrink. - Any other tips would be very helpful at this stage.
Cheers and Beers