Hello,
I make simple multiplayer RTS (lockstep) just for my training. Simulation is carried out with a fixed step on each client. Clients send each other only messages about their actions (button is pressed, spell is used, etc.).
But this requires a complete determination of the calculations.
On simple tests (random, division, addition, even LookAt), I saw complete determination and the same results after hundreds of iterations.
Then I started making a simple simulation of enemy movement based on BoidSample and in the process decided to check the determination. And the results did not match at all.
- There are 2 spawners on scene. Each spawner creates 1000 enemies in random places. Each spawner has an enemy prefab that has its own ID in SharedComponentData. As a result, yellow and purple capsules are created. All yellow capsules have the same positions as the purple capsules. That is, the input is the same.
Yellow capsules visually overlap purple.
2. All enemies go to the center, trying to avoid collisions (Steer Job). The system performs separate calculations for each group with one ID (as in the BoidSample example).
3. It is expected that after N iterations, enemies with different IDs will be in the same positions. But:
10 seconds
100 seconds
It can be seen that at the beginning all the input data are the same, but after a while the capsules are in completely different places. The more time has passed, the more calculations are done, the greater the difference.
Other systems do not affect enemies.
I know that FloatMode.Deterministic is not ready yet, but should be soon (according to forum posts).
Is there DOTS determinism now? Perhaps with some reasonable restrictions? Maybe I can’t use some math operations? Or do I need to wait while FloatMode.Deterministic will work?
Or am I wrong and my code does not allow determinism?
public class Spawner : MonoBehaviour
{
public GameObject go;
public int count;
public void Update()
{
if (!Input.GetKeyDown(KeyCode.Space)) return;
var settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null);
var prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(go, settings);
var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
var rnd = new Unity.Mathematics.Random();
rnd.InitState();
for (var x = 0; x < count; x++)
{
var instance = entityManager.Instantiate(prefab);
var position = rnd.NextFloat3Direction() * 30;
position.y = 0;
entityManager.SetComponentData(instance, new Translation { Value = position });
}
}
}
using System.Collections.Generic;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Burst;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateBefore(typeof(TransformSystemGroup))]
public class BoidSystem : SystemBase
{
EntityQuery m_BoidQuery;
List<BoidShipComponent> m_UniqueTypes = new List<BoidShipComponent>(3);
[BurstCompile(FloatMode = FloatMode.Deterministic, FloatPrecision = FloatPrecision.High)]
struct MergeCells : IJobNativeMultiHashMapMergedSharedKeyIndices
{
public NativeArray<int> cellIndices;
public NativeArray<float3> cellAlignment;
public NativeArray<float3> cellSeparation;
public NativeArray<int> cellCount;
public void ExecuteFirst(int index)
{
cellIndices[index] = index;
}
public void ExecuteNext(int cellIndex, int index)
{
cellCount[cellIndex] += 1;
cellAlignment[cellIndex] = cellAlignment[cellIndex] + cellAlignment[index];
cellSeparation[cellIndex] = cellSeparation[cellIndex] + cellSeparation[index];
cellIndices[index] = cellIndex;
}
}
[BurstCompile(FloatMode = FloatMode.Deterministic, FloatPrecision = FloatPrecision.High)]
struct Steer : IJobForEachWithEntity<Rotation, Translation>
{
[ReadOnly] public NativeArray<int> cellIndices;
[ReadOnly] public NativeArray<int> cellCount;
[ReadOnly] public NativeArray<float3> cellAlignment;
[ReadOnly] public NativeArray<float3> cellSeparation;
[ReadOnly] public float deltaTime;
public void Execute(Entity entity, int entityInQueryIndex,ref Rotation rotation, ref Translation translation)
{
var cellIndex = cellIndices[entityInQueryIndex];
var forward = math.normalize(math.rotate(rotation.Value, new float3(0, 0, 1)));
var currentPosition = translation.Value;
var neighborCount = cellCount[cellIndex];
var alignment = cellAlignment[cellIndex];
var separation = cellSeparation[cellIndex];
var alignmentResult = 1
* math.normalizesafe((alignment / neighborCount) - forward);
var separationResult = 500
* math.normalizesafe((currentPosition * neighborCount) - separation);
var targetHeading = 50
* math.normalizesafe(new float3(0, 0, 0) - currentPosition);
var normalHeading = math.normalizesafe(alignmentResult + separationResult + targetHeading);
var targetForward = normalHeading;
var nextHeading = math.normalizesafe(forward + deltaTime * (targetForward - forward));
nextHeading.y = 0;
rotation.Value = quaternion.LookRotation(nextHeading, math.up());
var velocity = nextHeading;
translation.Value += velocity * deltaTime;
}
}
protected override void OnUpdate()
{
m_UniqueTypes = new List<BoidShipComponent>();
EntityManager.GetAllUniqueSharedComponentData(m_UniqueTypes);
for (int i = 0; i < m_UniqueTypes.Count; i++)
{
var settings = m_UniqueTypes[i];
m_BoidQuery.ResetFilter();
m_BoidQuery.AddSharedComponentFilter(settings);
var boidCount = m_BoidQuery.CalculateEntityCount();
if (boidCount == 0)
{
m_BoidQuery.ResetFilter();
continue;
}
var hashMap = new NativeMultiHashMap<int, int>(boidCount, Allocator.TempJob);
var cellIndices = new NativeArray<int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var cellCount = new NativeArray<int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var cellAlignment = new NativeArray<float3>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var cellSeparation = new NativeArray<float3>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var initialCellAlignmentJobHandle = Entities
.WithName("InitialCellAlignmentJob")
.WithStoreEntityQueryInField(ref m_BoidQuery)
.WithSharedComponentFilter(settings)
.WithBurst(FloatMode.Deterministic, FloatPrecision.High)
.ForEach((int entityInQueryIndex, in Rotation rotation) =>
{
cellAlignment[entityInQueryIndex] = math.normalize(math.rotate(rotation.Value, new float3(0, 0, 1)));
})
.ScheduleParallel(Dependency);
var initialCellSeparationJobHandle = Entities
.WithName("InitialCellSeparationJob")
.WithStoreEntityQueryInField(ref m_BoidQuery)
.WithSharedComponentFilter(settings)
.WithBurst(FloatMode.Deterministic, FloatPrecision.High)
.ForEach((int entityInQueryIndex, in Translation translation) =>
{
cellSeparation[entityInQueryIndex] = translation.Value;
})
.ScheduleParallel(Dependency);
var parallelHashMap = hashMap.AsParallelWriter();
var hashPositionsJobHandle = Entities
.WithName("HashPositionsJob")
.WithStoreEntityQueryInField(ref m_BoidQuery)
.WithSharedComponentFilter(settings)
.WithBurst(FloatMode.Deterministic, FloatPrecision.High)
.ForEach((int entityInQueryIndex, in Translation translation) =>
{
var hash = (int)math.hash(new int3(math.floor(translation.Value)));
parallelHashMap.Add(hash, entityInQueryIndex);
})
.ScheduleParallel(Dependency);
var initialCellCountJob = new MemsetNativeArray<int>
{
Source = cellCount,
Value = 1
};
var initialCellCountJobHandle = initialCellCountJob.Schedule(boidCount, 64, Dependency);
var initialCellBarrierJobHandle = JobHandle.CombineDependencies(initialCellAlignmentJobHandle, initialCellSeparationJobHandle, initialCellCountJobHandle);
var mergeCellsBarrierJobHandle = JobHandle.CombineDependencies(hashPositionsJobHandle, initialCellBarrierJobHandle, initialCellBarrierJobHandle);
var mergeCellsJob = new MergeCells
{
cellIndices = cellIndices,
cellAlignment = cellAlignment,
cellSeparation = cellSeparation,
cellCount = cellCount,
};
var mergeCellsJobHandle = mergeCellsJob.Schedule(hashMap, 64, mergeCellsBarrierJobHandle);
float deltaTime = math.min(0.05f, UnityEngine.Time.fixedDeltaTime);
var steerJob = new Steer
{
cellAlignment = cellAlignment,
cellCount = cellCount,
cellIndices = cellIndices,
cellSeparation = cellSeparation,
deltaTime = deltaTime,
};
var steerJobHandle = steerJob.Schedule(m_BoidQuery, mergeCellsJobHandle);
Dependency = steerJobHandle;
var disposeJobHandle = hashMap.Dispose(Dependency);
disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellIndices.Dispose(Dependency));
disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellCount.Dispose(Dependency));
disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellAlignment.Dispose(Dependency));
disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellSeparation.Dispose(Dependency));
Dependency = disposeJobHandle;
m_BoidQuery.AddDependency(Dependency);
}
}
protected override void OnCreate()
{
m_BoidQuery = GetEntityQuery(new EntityQueryDesc
{
All = new[] { ComponentType.ReadOnly<BoidShipComponent>(), ComponentType.ReadWrite<Rotation>(), ComponentType.ReadWrite<Translation>() },
});
RequireForUpdate(m_BoidQuery);
}
}
P.S. The code is bad and was quickly written only for tests.