protected override void OnUpdate() { var enemyCount = m_EnemyQuery.CalculateEntityCount(); var targetCount = m_TargetQuery.CalculateEntityCount(); EntityManager.GetAllUniqueSharedComponentData(m_UniqueTypes); // Each variant of the Boid represents a different value of the SharedComponentData and is self-contained, // meaning Boids of the same variant only interact with one another. Thus, this loop processes each // variant type individually. for (int boidVariantIndex = 0; boidVariantIndex < m_UniqueTypes.Count; boidVariantIndex++) { var settings = m_UniqueTypes[boidVariantIndex]; m_BoidQuery.AddSharedComponentFilter(settings); var boidCount = m_BoidQuery.CalculateEntityCount(); if (boidCount == 0) { // Early out. If the given variant includes no Boids, move on to the next loop. // For example, variant 0 will always exit early bc it's it represents a default, uninitialized // Boid struct, which does not appear in this sample. m_BoidQuery.ResetFilter(); continue; } // The following calculates spatial cells of neighboring Boids // note: working with a sparse grid and not a dense bounded grid so there // are no predefined borders of the space. var hashMap = new NativeMultiHashMap <int, int>(boidCount, Allocator.TempJob); var cellIndices = new NativeArray <int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var cellEnemyPositionIndex = new NativeArray <int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var cellTargetPositionIndex = new NativeArray <int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var cellCount = new NativeArray <int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var cellEnemyDistance = new NativeArray <float>(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 raycastInputs = new NativeArray <RaycastInput>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var initialRaycastResults = new NativeArray <RaycastHit>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var unobstructedDirections = new NativeArray <float3>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var copyTargetPositions = new NativeArray <float3>(targetCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var copyEnemyPositions = new NativeArray <float3>(enemyCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); // The following jobs all run in parallel because the same JobHandle is passed for their // input dependencies when the jobs are scheduled; thus, they can run in any order (or concurrently). // The concurrency is property of how they're scheduled, not of the job structs themselves. // These jobs extract the relevant position, heading component // to NativeArrays so that they can be randomly accessed by the `MergeCells` and `Steer` jobs. // These jobs are defined inline using the Entities.ForEach lambda syntax. var initialCellAlignmentJobHandle = Entities .WithSharedComponentFilter(settings) .WithName("InitialCellAlignmentJob") .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) => { cellAlignment[entityInQueryIndex] = localToWorld.Forward; }) .ScheduleParallel(Dependency); var initialCellSeparationJobHandle = Entities .WithSharedComponentFilter(settings) .WithName("InitialCellSeparationJob") .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) => { cellSeparation[entityInQueryIndex] = localToWorld.Position; }) .ScheduleParallel(Dependency); var initialRaycastInputsJobHandle = Entities .WithSharedComponentFilter(settings) .WithName("InitialRaycastInputsJob") .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) => { raycastInputs[entityInQueryIndex] = new RaycastInput { Start = localToWorld.Position, End = localToWorld.Position + (localToWorld.Forward * settings.OuterDetectionRadius), Filter = new CollisionFilter { BelongsTo = ~0u, CollidesWith = 1, // Environment layer GroupIndex = 0 }, }; }) .ScheduleParallel(Dependency); var world = World.DefaultGameObjectInjectionWorld.GetExistingSystem <Unity.Physics.Systems.BuildPhysicsWorld>().PhysicsWorld.CollisionWorld; var batchRaycastJobHandle = ScheduleBatchRayCast(world, raycastInputs, initialRaycastResults, initialRaycastInputsJobHandle); var findUnobstructedDirectionsJobHandle = Entities .WithName("FindUnobstructedDirectionsJob") .WithSharedComponentFilter(settings) .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld, in DynamicBuffer <Float3BufferElement> buffer) => { JobLogger.Log("In find unobstructed job"); float3 bestDir = float3.zero; float furthestHit = 0f; RaycastHit hit; DynamicBuffer <float3> float3buffer = buffer.Reinterpret <float3>(); for (int i = 0; i < float3buffer.Length; i++) { float3 end = localToWorld.Position + ((math.mul(localToWorld.Value, new float4(float3buffer[i], 1)) * settings.OuterDetectionRadius)).xyz; RaycastInput input = new RaycastInput() { Start = localToWorld.Position, End = end, Filter = new CollisionFilter { BelongsTo = ~0u, CollidesWith = 1, // Environment layer GroupIndex = 0 }, }; if (world.CastRay(input, out hit)) { var dist = math.distance(hit.Position, localToWorld.Position); if (dist > furthestHit) { bestDir = hit.Position - localToWorld.Position; furthestHit = dist; JobLogger.Log("Found a better way"); } } else // this direction is unobstructed, return { unobstructedDirections[entityInQueryIndex] = hit.Position - localToWorld.Position; JobLogger.Log("Found a way"); return; } } unobstructedDirections[entityInQueryIndex] = bestDir; }).ScheduleParallel(batchRaycastJobHandle);
protected override void OnUpdate() { var obstacleCount = m_ObstacleQuery.CalculateEntityCount(); var targetCount = m_TargetQuery.CalculateEntityCount(); EntityManager.GetAllUniqueSharedComponentData(m_UniqueTypes); // Each variant of the Boid represents a different value of the SharedComponentData and is self-contained, // meaning Boids of the same variant only interact with one another. Thus, this loop processes each // variant type individually. for (int boidVariantIndex = 0; boidVariantIndex < m_UniqueTypes.Count; boidVariantIndex++) { var settings = m_UniqueTypes[boidVariantIndex]; m_BoidQuery.AddSharedComponentFilter(settings); var boidCount = m_BoidQuery.CalculateEntityCount(); if (boidCount == 0) { // Early out. If the given variant includes no Boids, move on to the next loop. // For example, variant 0 will always exit early bc it's it represents a default, uninitialized // Boid struct, which does not appear in this sample. m_BoidQuery.ResetFilter(); continue; } // The following calculates spatial cells of neighboring Boids // note: working with a sparse grid and not a dense bounded grid so there // are no predefined borders of the space. var hashMap = new NativeMultiHashMap <int, int>(boidCount, Allocator.TempJob); var cellIndices = new NativeArray <int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var cellObstaclePositionIndex = new NativeArray <int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var cellTargetPositionIndex = new NativeArray <int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var cellCount = new NativeArray <int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var cellObstacleDistance = new NativeArray <float>(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 copyTargetPositions = new NativeArray <float3>(targetCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var copyObstaclePositions = new NativeArray <float3>(obstacleCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); // The following jobs all run in parallel because the same JobHandle is passed for their // input dependencies when the jobs are scheduled; thus, they can run in any order (or concurrently). // The concurrency is property of how they're scheduled, not of the job structs themselves. // These jobs extract the relevant position, heading component // to NativeArrays so that they can be randomly accessed by the `MergeCells` and `Steer` jobs. // These jobs are defined inline using the Entities.ForEach lambda syntax. var initialCellAlignmentJobHandle = Entities .WithSharedComponentFilter(settings) .WithName("InitialCellAlignmentJob") .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) => { cellAlignment[entityInQueryIndex] = localToWorld.Forward; }) .ScheduleParallel(Dependency); var initialCellSeparationJobHandle = Entities .WithSharedComponentFilter(settings) .WithName("InitialCellSeparationJob") .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) => { cellSeparation[entityInQueryIndex] = localToWorld.Position; }) .ScheduleParallel(Dependency); var copyTargetPositionsJobHandle = Entities .WithName("CopyTargetPositionsJob") .WithAll <BoidTarget>() .WithStoreEntityQueryInField(ref m_TargetQuery) .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) => { copyTargetPositions[entityInQueryIndex] = localToWorld.Position; }) .ScheduleParallel(Dependency); var copyObstaclePositionsJobHandle = Entities .WithName("CopyObstaclePositionsJob") .WithAll <BoidObstacle>() .WithStoreEntityQueryInField(ref m_ObstacleQuery) .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) => { copyObstaclePositions[entityInQueryIndex] = localToWorld.Position; }) .ScheduleParallel(Dependency); // Populates a hash map, where each bucket contains the indices of all Boids whose positions quantize // to the same value for a given cell radius so that the information can be randomly accessed by // the `MergeCells` and `Steer` jobs. // This is useful in terms of the algorithm because it limits the number of comparisons that will // actually occur between the different boids. Instead of for each boid, searching through all // boids for those within a certain radius, this limits those by the hash-to-bucket simplification. var parallelHashMap = hashMap.AsParallelWriter(); var hashPositionsJobHandle = Entities .WithName("HashPositionsJob") .WithAll <Boid>() .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) => { var hash = (int)math.hash(new int3(math.floor(localToWorld.Position / settings.CellRadius))); 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 copyTargetObstacleBarrierJobHandle = JobHandle.CombineDependencies(copyTargetPositionsJobHandle, copyObstaclePositionsJobHandle); var mergeCellsBarrierJobHandle = JobHandle.CombineDependencies(hashPositionsJobHandle, initialCellBarrierJobHandle, copyTargetObstacleBarrierJobHandle); var mergeCellsJob = new MergeCells { cellIndices = cellIndices, cellAlignment = cellAlignment, cellSeparation = cellSeparation, cellObstacleDistance = cellObstacleDistance, cellObstaclePositionIndex = cellObstaclePositionIndex, cellTargetPositionIndex = cellTargetPositionIndex, cellCount = cellCount, targetPositions = copyTargetPositions, obstaclePositions = copyObstaclePositions }; var mergeCellsJobHandle = mergeCellsJob.Schedule(hashMap, 64, mergeCellsBarrierJobHandle); // This reads the previously calculated boid information for all the boids of each cell to update // the `localToWorld` of each of the boids based on their newly calculated headings using // the standard boid flocking algorithm. float deltaTime = math.min(0.05f, Time.DeltaTime); var steerJobHandle = Entities .WithName("Steer") .WithSharedComponentFilter(settings) // implies .WithAll<Boid>() .WithReadOnly(cellIndices) .WithReadOnly(cellCount) .WithReadOnly(cellAlignment) .WithReadOnly(cellSeparation) .WithReadOnly(cellObstacleDistance) .WithReadOnly(cellObstaclePositionIndex) .WithReadOnly(cellTargetPositionIndex) .WithReadOnly(copyObstaclePositions) .WithReadOnly(copyTargetPositions) .ForEach((int entityInQueryIndex, ref LocalToWorld localToWorld) => { // temporarily storing the values for code readability var forward = localToWorld.Forward; var currentPosition = localToWorld.Position; var cellIndex = cellIndices[entityInQueryIndex]; var neighborCount = cellCount[cellIndex]; var alignment = cellAlignment[cellIndex]; var separation = cellSeparation[cellIndex]; var nearestObstacleDistance = cellObstacleDistance[cellIndex]; var nearestObstaclePositionIndex = cellObstaclePositionIndex[cellIndex]; var nearestTargetPositionIndex = cellTargetPositionIndex[cellIndex]; var nearestObstaclePosition = copyObstaclePositions[nearestObstaclePositionIndex]; var nearestTargetPosition = copyTargetPositions[nearestTargetPositionIndex]; // Setting up the directions for the three main biocrowds influencing directions adjusted based // on the predefined weights: // 1) alignment - how much should it move in a direction similar to those around it? // note: we use `alignment/neighborCount`, because we need the average alignment in this case; however // alignment is currently the summation of all those of the boids within the cellIndex being considered. var alignmentResult = settings.AlignmentWeight * math.normalizesafe((alignment / neighborCount) - forward); // 2) separation - how close is it to other boids and are there too many or too few for comfort? // note: here separation represents the summed possible center of the cell. We perform the multiplication // so that both `currentPosition` and `separation` are weighted to represent the cell as a whole and not // the current individual boid. var separationResult = settings.SeparationWeight * math.normalizesafe((currentPosition * neighborCount) - separation); // 3) target - is it still towards its destination? var targetHeading = settings.TargetWeight * math.normalizesafe(nearestTargetPosition - currentPosition); // creating the obstacle avoidant vector s.t. it's pointing towards the nearest obstacle // but at the specified 'ObstacleAversionDistance'. If this distance is greater than the // current distance to the obstacle, the direction becomes inverted. This simulates the // idea that if `currentPosition` is too close to an obstacle, the weight of this pushes // the current boid to escape in the fastest direction; however, if the obstacle isn't // too close, the weighting denotes that the boid doesnt need to escape but will move // slower if still moving in that direction (note: we end up not using this move-slower // case, because of `targetForward`'s decision to not use obstacle avoidance if an obstacle // isn't close enough). var obstacleSteering = currentPosition - nearestObstaclePosition; var avoidObstacleHeading = (nearestObstaclePosition + math.normalizesafe(obstacleSteering) * settings.ObstacleAversionDistance) - currentPosition; // the updated heading direction. If not needing to be avoidant (ie obstacle is not within // predefined radius) then go with the usual defined heading that uses the amalgamation of // the weighted alignment, separation, and target direction vectors. var nearestObstacleDistanceFromRadius = nearestObstacleDistance - settings.ObstacleAversionDistance; var normalHeading = math.normalizesafe(alignmentResult + separationResult + targetHeading); var targetForward = math.select(normalHeading, avoidObstacleHeading, nearestObstacleDistanceFromRadius < 0); // updates using the newly calculated heading direction var nextHeading = math.normalizesafe(forward + deltaTime * (targetForward - forward)); localToWorld = new LocalToWorld { Value = float4x4.TRS( new float3(localToWorld.Position + (nextHeading * settings.MoveSpeed * deltaTime)), quaternion.LookRotationSafe(nextHeading, math.up()), new float3(1.0f, 1.0f, 1.0f)) }; }).ScheduleParallel(mergeCellsJobHandle); // Dispose allocated containers with dispose jobs. Dependency = steerJobHandle; var disposeJobHandle = hashMap.Dispose(Dependency); disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellIndices.Dispose(Dependency)); disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellObstaclePositionIndex.Dispose(Dependency)); disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellTargetPositionIndex.Dispose(Dependency)); disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellCount.Dispose(Dependency)); disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellObstacleDistance.Dispose(Dependency)); disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellAlignment.Dispose(Dependency)); disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, cellSeparation.Dispose(Dependency)); disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, copyObstaclePositions.Dispose(Dependency)); disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, copyTargetPositions.Dispose(Dependency)); Dependency = disposeJobHandle; // We pass the job handle and add the dependency so that we keep the proper ordering between the jobs // as the looping iterates. For our purposes of execution, this ordering isn't necessary; however, without // the add dependency call here, the safety system will throw an error, because we're accessing multiple // pieces of boid data and it would think there could possibly be a race condition. m_BoidQuery.AddDependency(Dependency); m_BoidQuery.ResetFilter(); } m_UniqueTypes.Clear(); }
protected override void OnCreate() { _endSimulationEcbSystem = World.GetOrCreateSystem <EndSimulationEntityCommandBufferSystem>(); _unitSelectSystem = World.GetOrCreateSystem <UnitSelectionPrepSystem>(); _walkableLayer = 1u << 0; _unitQuery = GetEntityQuery( ComponentType.ReadOnly(typeof(UnitTag)), ComponentType.ReadWrite(typeof(TargetPosition)), ComponentType.ReadOnly(typeof(FormationIndex)), ComponentType.ReadOnly(typeof(Translation)) ); _selectedUnitQuery = GetEntityQuery( ComponentType.ReadOnly(typeof(UnitTag)), ComponentType.ReadOnly(typeof(TargetPosition)), ComponentType.ReadOnly(typeof(Translation)), ComponentType.ReadOnly(typeof(FormationIndex)), ComponentType.ReadOnly(typeof(SelectedFormationSharedComponent)), ComponentType.ReadOnly(typeof(FormationGroup)) ); _selectedUnitQuery.AddSharedComponentFilter(new SelectedFormationSharedComponent { IsSelected = true }); _selectedFormationUnitQuery = GetEntityQuery( ComponentType.ReadOnly(typeof(UnitTag)), ComponentType.ReadOnly(typeof(FormationIndex)), ComponentType.ReadOnly(typeof(SelectedFormationSharedComponent)), ComponentType.ReadOnly(typeof(Translation)) ); _selectedFormationUnitQuery.AddSharedComponentFilter(new SelectedFormationSharedComponent { IsSelected = true }); _unitDragIndicatorQuery = GetEntityQuery( ComponentType.ReadOnly(typeof(DragIndicatorTag)) ); _selectedDragIndicatorQuery = GetEntityQuery( ComponentType.ReadOnly(typeof(DragIndicatorTag)), ComponentType.ReadWrite(typeof(SelectedFormationSharedComponent)) ); _selectedDragIndicatorQuery.AddSharedComponentFilter(new SelectedFormationSharedComponent { IsSelected = true }); var enabledDragIndicatorQueryDesc = new EntityQueryDesc { None = new ComponentType[] { typeof(DisableRendering) }, All = new ComponentType[] { typeof(DragIndicatorTag) } }; _enabledDragIndicatorQuery = GetEntityQuery(enabledDragIndicatorQueryDesc); _disabledDragIndicatorQuery = GetEntityQuery( ComponentType.ReadOnly(typeof(DragIndicatorTag)), ComponentType.ReadWrite(typeof(DisableRendering)) ); }