#pragma warning disable 0282 // Allows the 'partial' keyword without warnings using UnityEngine; using System.Collections.Generic; #if MODULE_ENTITIES using Pathfinding.RVO; using Unity.Jobs; using Unity.Mathematics; using Unity.Burst; using Unity.Collections; using UnityEngine.Rendering; using Unity.Entities; using Unity.Transforms; namespace Pathfinding.Examples { using Pathfinding.ECS; using Pathfinding.ECS.RVO; using Pathfinding.Util; /// /// Lightweight example script for simulating rvo agents. /// /// This script, compared to using lots of RVOController components shows the real power of the RVO simulator when /// little other overhead (e.g GameObjects and pathfinding) is present. /// /// With this script, I can simulate 30 000 agents at over 60 fps on my, admittedly quite beefy, machine (in a standalone build, with the local avoidance simulation running at a fixed 60 fps and using up to 14 cores of my machine). /// This is significantly more than one can simulate when using GameObjects for each agent. /// /// This script will render the agents by generating a square for each agent combined into a single mesh with appropriate UV. /// /// A few GUI buttons will be drawn by this script with which the user can change the number of agents. /// /// [Open online documentation to see images] /// /// Video: https://www.youtube.com/watch?v=wxzrHRIiVyk /// /// See: local-avoidance (view in online documentation for working links) /// public partial class LightweightRVO : MonoBehaviour { /// Number of agents created at start public int agentCount = 100; /// How large is the area in which the agents are distributed when starting the simulation public float exampleScale = 100; public enum RVOExampleType { Circle, Line, Point, RandomStreams, Crossing } /// How the agents are distributed when starting the simulation public RVOExampleType type = RVOExampleType.Circle; /// Agent radius public float radius = 3; /// Max speed for an agent public float maxSpeed = 2; /// How far in the future too look for agents public float agentTimeHorizon = 10; [HideInInspector] /// How far in the future too look for obstacles public float obstacleTimeHorizon = 10; /// Max number of neighbour agents to take into account public int maxNeighbours = 10; /// /// Offset from the agent position the actual drawn postition. /// Used to get rid of z-buffer issues /// public Vector3 renderingOffset = Vector3.up*0.1f; /// Bitmas of debugging options to enable for the agents public AgentDebugFlags debug; public Material material; public void Start () { CreateAgents(agentCount); // Create the systems and add them to their respective simulation groups // Normally this is handled automatically by Unity, but we use the [DisableAutoCreation] attribute // since these systems are only used in an example scene. var world = World.DefaultGameObjectInjectionWorld; var simulationGroup = world.GetOrCreateSystemManaged(); simulationGroup.AddSystemToUpdateList(world.CreateSystem()); simulationGroup.AddSystemToUpdateList(world.CreateSystem()); var renderSystem = world.AddSystemManaged(new LightweightRVORenderSystem { material = material, renderingOffset = renderingOffset, }); world.GetOrCreateSystemManaged().AddSystemToUpdateList(renderSystem); // Annoyingly, the PresentationSystemGroup is not called when the game is paused. // So we need to render the mesh from a different callback, otherwise the mesh would // disappear when pausing the game. // To add additional complexity, we need different callbacks depending on if // we are using the built-in render pipeline or a scriptable render pipeline. // Callback when rendering with the built-in render pipeline Camera.onPreCull += PreCull; // Callback when rendering with a scriptable render pipeline #if UNITY_2023_3_OR_NEWER RenderPipelineManager.beginContextRendering += OnBeginContextRendering; #else RenderPipelineManager.beginFrameRendering += OnBeginFrameRendering; #endif } #if UNITY_2023_3_OR_NEWER void OnBeginContextRendering (ScriptableRenderContext ctx, List cameras) { for (int i = 0; i < cameras.Count; i++) PreCull(cameras[i]); } #else void OnBeginFrameRendering (ScriptableRenderContext ctx, Camera[] cameras) { for (int i = 0; i < cameras.Length; i++) PreCull(cameras[i]); } #endif void PreCull (Camera camera) { var world = World.DefaultGameObjectInjectionWorld; var mesh = world.GetOrCreateSystemManaged().mesh; // Render the mesh in the game Graphics.DrawMesh(mesh, Matrix4x4.identity, material, 0, camera); } void OnDestroy () { #if UNITY_2023_3_OR_NEWER RenderPipelineManager.beginContextRendering -= OnBeginContextRendering; #else RenderPipelineManager.beginFrameRendering -= OnBeginFrameRendering; #endif Camera.onPreCull -= PreCull; } public void OnGUI () { if (GUILayout.Button("2")) CreateAgents(2); if (GUILayout.Button("10")) CreateAgents(10); if (GUILayout.Button("100")) CreateAgents(100); if (GUILayout.Button("500")) CreateAgents(500); if (GUILayout.Button("1000")) CreateAgents(1000); if (GUILayout.Button("5000")) CreateAgents(5000); if (GUILayout.Button("10000")) CreateAgents(10000); if (GUILayout.Button("20000")) CreateAgents(20000); if (GUILayout.Button("30000")) CreateAgents(30000); GUILayout.Space(5); if (GUILayout.Button("Random Streams")) { type = RVOExampleType.RandomStreams; CreateAgents(agentCount); } if (GUILayout.Button("Line")) { type = RVOExampleType.Line; CreateAgents(Mathf.Min(agentCount, 100)); } if (GUILayout.Button("Circle")) { type = RVOExampleType.Circle; CreateAgents(agentCount); } if (GUILayout.Button("Point")) { type = RVOExampleType.Point; CreateAgents(agentCount); } if (GUILayout.Button("Crossing")) { type = RVOExampleType.Crossing; CreateAgents(agentCount); } } public void Update () { var world = World.DefaultGameObjectInjectionWorld; var system = world.GetOrCreateSystem(); world.Unmanaged.GetUnsafeSystemRef(system).debug = debug; } private float uniformDistance (float radius) { float v = UnityEngine.Random.value + UnityEngine.Random.value; if (v > 1) return radius * (2-v); else return radius * v; } /// Some agent data used in the lightweight rvo example scene public struct LightweightAgentData : IComponentData { public Color32 color; public float maxSpeed; } /// Create a single agent entity Entity CreateAgent (EntityArchetype archetype, EntityCommandBuffer buffer, Vector3 position, Vector3 destination, Color color, float priority = 0.5f) { var entity = buffer.CreateEntity(archetype); buffer.AddComponent(entity, LocalTransform.FromPosition(position)); buffer.AddComponent(entity, new DestinationPoint { destination = destination }); buffer.AddComponent(entity, new RVOAgent { agentTimeHorizon = agentTimeHorizon, obstacleTimeHorizon = obstacleTimeHorizon, maxNeighbours = maxNeighbours, layer = RVOLayer.DefaultAgent, collidesWith = (RVOLayer)(-1), priority = priority, priorityMultiplier = 1, flowFollowingStrength = 0, debug = AgentDebugFlags.Nothing, locked = false }); buffer.AddComponent(entity, new AgentMovementPlane { value = new NativeMovementPlane(quaternion.identity) }); buffer.AddComponent(entity, new AgentCylinderShape { radius = radius, height = 1.0f }); buffer.AddComponent(entity, new LightweightAgentData { color = (Color32)color, maxSpeed = maxSpeed }); return entity; } /// Create a number of agents in circle and restart simulation public void CreateAgents (int num) { this.agentCount = num; var world = World.DefaultGameObjectInjectionWorld; var entityManager = world.EntityManager; var archetype = entityManager.CreateArchetype( typeof(LocalTransform), typeof(LocalToWorld), typeof(AgentCylinderShape), typeof(ResolvedMovement), typeof(DestinationPoint), typeof(MovementControl), typeof(RVOAgent), typeof(AgentMovementPlane), typeof(LightweightAgentData), typeof(SimulateMovement), typeof(SimulateMovementRepair), typeof(SimulateMovementControl), typeof(SimulateMovementFinalize) ); var buffer = new EntityCommandBuffer(Allocator.Temp); var existingEntities = entityManager.CreateEntityQuery(typeof(LightweightAgentData)); #if MODULE_ENTITIES_1_0_8_OR_NEWER buffer.DestroyEntity(existingEntities, EntityQueryCaptureMode.AtPlayback); #else buffer.DestroyEntity(existingEntities); #endif if (type == RVOExampleType.Circle) { float agentArea = agentCount * radius * radius * Mathf.PI; const float EmptyFraction = 0.7f; const float PackingDensity = 0.9f; float innerCircleRadius = Mathf.Sqrt(agentArea/(Mathf.PI*(1-EmptyFraction*EmptyFraction))); float outerCircleRadius = Mathf.Sqrt(innerCircleRadius*innerCircleRadius + agentCount*radius*radius/PackingDensity); for (int i = 0; i < agentCount; i++) { Vector3 pos = new Vector3(Mathf.Cos(i * Mathf.PI * 2.0f / agentCount), 0, Mathf.Sin(i * Mathf.PI * 2.0f / agentCount)) * math.lerp(innerCircleRadius, outerCircleRadius, UnityEngine.Random.value); var destination = new float3(-pos.x, 0, -pos.z); var color = AstarMath.HSVToRGB(i * 360.0f / agentCount, 0.8f, 0.6f); CreateAgent(archetype, buffer, pos, destination, color); } } else if (type == RVOExampleType.Line) { for (int i = 0; i < agentCount; i++) { Vector3 pos = new Vector3((i % 2 == 0 ? 1 : -1) * exampleScale, 0, (i / 2) * radius * 2.5f); CreateAgent(archetype, buffer, pos, new float3(-pos.x, 0, pos.z), i % 2 == 0 ? Color.red : Color.blue); } } else if (type == RVOExampleType.Point) { for (int i = 0; i < agentCount; i++) { Vector3 pos = new Vector3(Mathf.Cos(i * Mathf.PI * 2.0f / agentCount), 0, Mathf.Sin(i * Mathf.PI * 2.0f / agentCount)) * exampleScale; CreateAgent(archetype, buffer, pos, new float3(0, 0, 0), AstarMath.HSVToRGB(i * 360.0f / agentCount, 0.8f, 0.6f)); } } else if (type == RVOExampleType.RandomStreams) { float circleRad = Mathf.Sqrt(agentCount * radius * radius * 4 / Mathf.PI) * exampleScale * 0.05f; for (int i = 0; i < agentCount; i++) { float angle = UnityEngine.Random.value * Mathf.PI * 2.0f; float targetAngle = UnityEngine.Random.value * Mathf.PI * 2.0f; Vector3 pos = new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle)) * uniformDistance(circleRad); var destination = new float3(Mathf.Cos(targetAngle), 0, Mathf.Sin(targetAngle)) * uniformDistance(circleRad); var color = AstarMath.HSVToRGB(targetAngle * Mathf.Rad2Deg, 0.8f, 0.6f); CreateAgent(archetype, buffer, pos, destination, color); } } else if (type == RVOExampleType.Crossing) { float distanceBetweenGroups = exampleScale * radius * 0.5f; int directions = (int)Mathf.Sqrt(agentCount / 25f); directions = Mathf.Max(directions, 2); const int AgentsPerDistance = 10; for (int i = 0; i < agentCount; i++) { float angle = ((i % directions)/(float)directions) * Mathf.PI * 2.0f; var dist = distanceBetweenGroups * ((i/(directions*AgentsPerDistance) + 1) + 0.3f*UnityEngine.Random.value); Vector3 pos = new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle)) * dist; var destination = math.normalizesafe(new float3(-pos.x, 0, -pos.z)) * distanceBetweenGroups * 3; var color = AstarMath.HSVToRGB(angle * Mathf.Rad2Deg, 0.8f, 0.6f); CreateAgent(archetype, buffer, pos, destination, color, priority: (i % directions) == 0 ? 1 : 0.01f); } } buffer.Playback(entityManager); } /// Lightweight example system for moving agents [UpdateAfter(typeof(RVOSystem))] [UpdateInGroup(typeof(AIMovementSystemGroup))] [DisableAutoCreation] public partial struct LightweightRVOMoveSystem : ISystem { EntityQuery entityQuery; public void OnCreate (ref SystemState state) { entityQuery = state.GetEntityQuery( ComponentType.ReadWrite(), ComponentType.ReadOnly(), ComponentType.ReadOnly() ); } public void OnUpdate (ref SystemState state) { state.Dependency = new JobMoveAgents { deltaTime = SystemAPI.Time.DeltaTime }.ScheduleParallel(entityQuery, state.Dependency); } [BurstCompile] partial struct JobMoveAgents : IJobEntity { public float deltaTime; public void Execute (ref LocalTransform transform, in AgentMovementPlane movementPlane, in ResolvedMovement resolvedMovement) { transform.Position += Pathfinding.ECS.JobMoveAgent.MoveWithoutGravity(ref transform, in resolvedMovement, in movementPlane, deltaTime); } } } /// /// Lightweight example system for controlling and rendering RVO agents. /// /// This system is not intended to be used for anything other than the RVO example scene, and perhaps for reference for a curious reader. /// /// It also relies on the and . /// [UpdateBefore(typeof(RVOSystem))] [UpdateInGroup(typeof(AIMovementSystemGroup))] [DisableAutoCreation] public partial struct LightweightRVOControlSystem : ISystem { /// Determines what kind of debug info the RVO system should render as gizmos public AgentDebugFlags debug; EntityQuery entityQueryDirection; EntityQuery entityQueryControl; public void OnCreate (ref SystemState state) { entityQueryDirection = state.GetEntityQuery( ComponentType.ReadWrite(), ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadOnly() ); entityQueryControl = state.GetEntityQuery( ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadWrite(), ComponentType.ReadWrite() ); } public void OnUpdate (ref SystemState state) { state.Dependency = new AlignAgentWithMovementDirectionJob { deltaTime = SystemAPI.Time.DeltaTime, rotationSpeed = 5, }.ScheduleParallel(entityQueryDirection, state.Dependency); state.Dependency = new JobControlAgents { deltaTime = SystemAPI.Time.DeltaTime, debug = debug, }.Schedule(entityQueryControl, state.Dependency); } /// /// Job to set the direction each agent wants to move in. /// /// The will then try to move the agent in that direction, but taking care to avoid other agents and obstacles. /// [BurstCompile] public partial struct JobControlAgents : IJobEntity { public float deltaTime; public AgentDebugFlags debug; public void Execute (in LightweightAgentData agentData, in DestinationPoint destination, ref RVOAgent rvoAgent, ref MovementControl movementControl, [EntityIndexInQuery] int index) { movementControl = new MovementControl { // This is the point the agent will try to move towards targetPoint = destination.destination, endOfPath = destination.destination, speed = agentData.maxSpeed, // Allow the agent to move slightly faster than its desired speed if necessary maxSpeed = agentData.maxSpeed * 1.1f, // We don't have a graph, so this field is not relevant hierarchicalNodeIndex = -1, targetRotation = 0, targetRotationOffset = 0, rotationSpeed = 0, overrideLocalAvoidance = false, }; if (index == 0) { // Show most debug info only for the first agent, to reduce clutter rvoAgent.debug = debug; } else { rvoAgent.debug = debug & AgentDebugFlags.ReachedState; } } } /// Job to update each agent's position and rotation based on its movement direction [BurstCompile(FloatMode = FloatMode.Fast)] public partial struct AlignAgentWithMovementDirectionJob : IJobEntity { public float deltaTime; public float rotationSpeed; public void Execute (ref LocalTransform transform, in AgentCylinderShape shape, in AgentMovementPlane movementPlane, in MovementControl movementControl, in ResolvedMovement resolvedMovement) { if (resolvedMovement.speed > shape.radius*0.01f) { var speedFraction = math.sqrt(math.clamp(resolvedMovement.speed / movementControl.maxSpeed, 0, 1)); // If the agent is moving, align it with the movement direction var actualDirection = movementPlane.value.ToPlane(resolvedMovement.targetPoint - transform.Position); var actualAngle = math.atan2(actualDirection.y, actualDirection.x) - math.PI*0.5f; var targetRotation = movementPlane.value.ToWorldRotation(actualAngle); transform.Rotation = math.slerp(transform.Rotation, targetRotation, deltaTime*speedFraction*rotationSpeed); } } } } /// /// System to render RVO agents on a mesh. /// /// The system does not do any rendering itself, but only writes to the field. /// [DisableAutoCreation] public partial class LightweightRVORenderSystem : SystemBase { /// Mesh for rendering public Mesh mesh; /// Material for rendering public Material material; /// Offset with which to render the mesh from the agent's original positions public Vector3 renderingOffset; EntityQuery entityQuery; protected override void OnCreate () { mesh = new Mesh { name = "RVO Agents", }; entityQuery = GetEntityQuery( ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadOnly() ); } protected override void OnDestroy () { Mesh.Destroy(mesh); } protected override void OnUpdate () { var agentCount = entityQuery.CalculateEntityCount(); var vertexCount = agentCount*4; var indexCount = agentCount*6; var vertices = CollectionHelper.CreateNativeArray(vertexCount, WorldUpdateAllocator); var tris = CollectionHelper.CreateNativeArray(indexCount, WorldUpdateAllocator); Dependency = new JobGenerateMesh { verts = vertices, tris = tris, renderingOffset = renderingOffset }.Schedule(entityQuery, Dependency); // Specify the layout of each vertex. This should match the Vertex struct var layout = new[] { new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3), new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4), new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2), }; mesh.SetVertexBufferParams(vertexCount, layout); // To allow for more than ≈16k agents we need to use a 32 bit format for the mesh mesh.SetIndexBufferParams(indexCount, IndexFormat.UInt32); // Wait for the JobGenerateMesh job to complete before we try to use the mesh data Dependency.Complete(); // Set the vertex and index data mesh.SetVertexBufferData(vertices, 0, 0, vertices.Length); mesh.SetIndexBufferData(tris, 0, 0, tris.Length); mesh.subMeshCount = 1; mesh.SetSubMesh(0, new SubMeshDescriptor(0, tris.Length, MeshTopology.Triangles), MeshUpdateFlags.DontRecalculateBounds); // SetSubMesh doesn't seem to update the bounds properly for some reason, so we do it manually instead mesh.RecalculateBounds(); } [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] public struct Vertex { public float3 position; public Color32 color; public float2 uv; } /// /// Generates a simple mesh for rendering the agents. /// Each agent is a quad rotated and positioned to align with the agent. /// [BurstCompile(FloatMode = FloatMode.Fast)] public partial struct JobGenerateMesh : IJobEntity { [WriteOnly] public NativeArray verts; [WriteOnly] public NativeArray tris; public Vector3 renderingOffset; public void Execute (in LocalTransform transform, in LightweightAgentData agentData, in AgentCylinderShape shape, [EntityIndexInQuery] int entityIndex) { // Create a square with the "forward" direction along the agent's velocity float3 forward = transform.Forward() * shape.radius; if (math.all(forward == 0)) forward = new float3(0, 0, shape.radius); float3 right = math.cross(new float3(0, 1, 0), forward); float3 orig = transform.Position + (float3)renderingOffset; int vc = 4*entityIndex; int tc = 2*3*entityIndex; Color32 color = agentData.color; verts[vc+0] = new Vertex { position = (orig + forward - right), uv = new float2(0, 1), color = color, }; verts[vc+1] = new Vertex { position = (orig + forward + right), uv = new float2(1, 1), color = color, }; verts[vc+2] = new Vertex { position = (orig - forward + right), uv = new float2(1, 0), color = color, }; verts[vc+3] = new Vertex { position = (orig - forward - right), uv = new float2(0, 0), color = color, }; tris[tc+0] = (vc + 0); tris[tc+1] = (vc + 1); tris[tc+2] = (vc + 2); tris[tc+3] = (vc + 0); tris[tc+4] = (vc + 2); tris[tc+5] = (vc + 3); } } } } } #else namespace Pathfinding.Examples { [HelpURL("https://arongranberg.com/astar/documentation/stable/lightweightrvo.html")] public class LightweightRVO : MonoBehaviour { public void Start () { Debug.LogError("Lightweight RVO example script requires the entities package to be installed."); } } } #endif