Fair enough, it’s also not super urgent, would just be a big quality of life improvement
I updated mine to Entities 1.2 a few days ago if you want to use it.
That’s very kind, I adjusted my authoring script also a few weeks back to work with entities 1.2 and pathfinding 5.1, so all good at the moment. But thanks anyways.
Do you by any chance also get a lot of errors like:
Caught in a potentially infinite loop. The navmesh probably contains degenerate geometry.
I tried also your authoring scripts, but the same thing happens. I’m not sure when this error started to show up frequently, maybe around upgrade 5.09?
No I have never seen that error before. I’m using 5.1.0 with the Recast Graph.
Maybe try messing with some of the parameters in your navmesh and rebaking it?
Thanks. I think it must have something to do with the entities spawning, because I only get it the moment spawning them. The error is triggered by the Pathtracer which is called by the JobRepairPath. Maybe it’s because of an invalid path or starting position or alike.
Anyways, this is probably of topic…
If anyone wonders: Looks like this error came from setting an actual initial destination point in the authoring script instead of setting it to PositiveInfinity. At least now the error is gone.
Hey, I’m having this same issue regarding the infinite loop. Can you expand on what you mean by setting the initial destination point? On start I’m setting followerAi.destination = Vector3.positiveInfinity;
but still running into the errors unfortunately.
edit: just realized this thread is about ECS. Looks like I have the same problem outside of ECS, so I’m not sure I’ll be able to apply the same fix.
Yes setting the destination to positiveInfinity (in ecs case float3(float.positiveInfinity,float.positiveInfinity,float.positiveInfinity) fixed 90% of cases of this error. I also still get them now and then, but not that frequently anymore.
But I agree there seems to be a underlying problem which causes this error and it might be worth investigating further. But probably better in a new dedicated thread.
To clarify, currently Entities with Dots physics is not supported?
So to carve the navmesh an Entity requires a paired GameObject with old Physics component?
As far as I’m aware: I’m afraid so.
You’re gonna need basically duplicate colliders for everything navmesh related…
I have an A*PFP’s local avoidance and pathfinding project working with Entities and DOTS physics in Unity 6. It works well as far as I can tell (with NO game-objects except for the AStarPath). However, for navmesh cutting/carving, I have not found a solution without tethering a gameobject (NavmeshCut+Collider) to every entity that you want to be a navmesh cutter. I’m also using the Project Dawn Agents Navigation asset and I once asked the author (Lukas) about it and he said it has the same issue - you pretty much have to tether a game object (NavMeshObstacle) to every navmesh carving entity. It sucks, but it’s all we have right now. One of these days tertle in the Unity forums will release his pure ECS pathfinding, navmesh building, and local avoidance solution called Traverse which won’t require tethered game-objects.
I found this ECS implementation
But it just uses Unity new Navigation system.
So it definitely can be done, I am just not tech savvy enough to do it for this Astar.
That solution is adequate if you don’t care about local avoidance. I’m pretty sure it also has the same navmesh carving mechanism which requires tethered game-objects. Unfortunately it uses the “New” unity navigation system (com.unity.ai.navigation, released less than a year ago) which is now deprecated, and there is no replacement yet for a low-level navmesh API
I am also working on an authoring component, so far its working well. The performance of pure ECS has me blown away, those transform syncs add up when you get to a few thousand units.
For this to work you have to add [ChunkSerializable] to the ManagedState class or the authoring Baker isn’t able to add it.
using Pathfinding.ECS;
using Pathfinding.PID;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
namespace Pathfinding
{
public class FollowerEntityAuthoring : MonoBehaviour
{
[SerializeField]
AgentCylinderShape shape = new AgentCylinderShape
{
height = 2,
radius = 0.5f,
};
[SerializeField]
MovementSettings movement = new MovementSettings
{
follower = new PIDMovement
{
rotationSpeed = 600,
speed = 5,
maxRotationSpeed = 720,
maxOnSpotRotationSpeed = 720,
slowdownTime = 0.5f,
desiredWallDistance = 0.5f,
allowRotatingOnSpot = true,
leadInRadiusWhenApproachingDestination = 1f,
},
stopDistance = 0.2f,
rotationSmoothing = 0f,
groundMask = -1,
isStopped = false,
};
[SerializeField]
OrientationMode orientationBacking = OrientationMode.ZAxisForward;
[SerializeField]
MovementPlaneSource movementPlaneSourceBacking = MovementPlaneSource.Graph;
[SerializeField]
public ManagedState managedState = new ManagedState
{
enableLocalAvoidance = false,
pathfindingSettings = PathRequestSettings.Default,
};
[SerializeField]
Pathfinding.ECS.AutoRepathPolicy autoRepathBacking = Pathfinding.ECS.AutoRepathPolicy.Default;
public class Baker : Baker<FollowerEntityAuthoring>
{
public override void Bake(FollowerEntityAuthoring authoring)
{
Entity entity = GetEntity(TransformUsageFlags.Dynamic);
var pos = authoring.transform.position;
//Seems like no initial are values required
AddComponent(entity, new MovementControl { });
AddComponent(entity, new SearchState { });
AddComponent(entity, new ResolvedMovement { });
AddComponent(entity, new SimulateMovement { });
AddComponent(entity, new SimulateMovementRepair { });
AddComponent(entity, new SimulateMovementControl { });
AddComponent(entity, new SimulateMovementFinalize { });
//The components for follower entity to function
AddComponent(entity, new MovementState(pos));
AddComponent(entity, new AgentMovementPlane(authoring.transform.rotation));
AddComponent(entity, new DestinationPoint
{
destination = new float3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity),
});
AddComponent(entity, authoring.autoRepathBacking);
AddComponent(entity, authoring.movement);
//We have to initialize the pathTracer temporarily in the Baker for it to succeed
//Since this points to the authoring GameObject's ManagedState it will get disposed anyways
var pathTracer = new PathTracer(Allocator.Temp);
if (!authoring.managedState.pathTracer.isCreated)
{
authoring.managedState.pathTracer = pathTracer;
}
AddComponentObject(entity, authoring.managedState);
AddComponent(entity, new ManagedStateInit { });
AddComponent(entity, new MovementStatistics
{
estimatedVelocity = float3.zero,
lastPosition = pos,
});
AddComponent(entity, authoring.shape);
AddComponent(entity, new GravityState { });
SetComponentEnabled<GravityState>(entity, authoring.managedState.enableGravity);
if (authoring.orientationBacking == OrientationMode.YAxisForward)
{
AddComponent<OrientationYAxisForward>(entity);
}
AddComponent(entity, new ReadyToTraverseOffMeshLink { });
SetComponentEnabled<ReadyToTraverseOffMeshLink>(entity, false);
AddSharedComponent(entity, new AgentMovementPlaneSource { value = authoring.movementPlaneSourceBacking });
pathTracer.Dispose();
}
}
}
//Tag to initialize ManagedState on an entity, the ManagedState should already be added to the entity
public struct ManagedStateInit : IComponentData { }
//Update in TransformSystemGroup ensures that the ManagedState is setup before any simulation occurs
[UpdateInGroup(typeof(TransformSystemGroup))]
public partial struct ManagedStateSetupSystem : ISystem
{
void OnUpdate(ref SystemState state)
{
var world = World.DefaultGameObjectInjectionWorld;
NativeList<Entity> removeEntities = new NativeList<Entity>(Allocator.Temp);
foreach (var (managedState, entity) in SystemAPI.Query<ManagedState>().WithAny<ManagedStateInit>().WithEntityAccess())
{
//The path tracer gets disposed in the authoring component we need to create it
if (!managedState.pathTracer.isCreated)
{
managedState.pathTracer = new PathTracer(Allocator.Persistent);
world.EntityManager.SetComponentData(entity, managedState);
}
removeEntities.Add(entity);
}
//Clear the init tag
world.EntityManager.RemoveComponent(removeEntities.AsArray(), typeof(ManagedStateInit));
removeEntities.Dispose();
}
}
}
Here is a tweaked editor script for the authoring component. Seems to work for my purposes but haven’t tested all the features. Its basically just a tweaked FollowerEntityEditor script.
using UnityEditor;
using UnityEngine;
using Pathfinding.RVO;
using Pathfinding.ECS;
using System.Linq;
namespace Pathfinding
{
[CustomEditor(typeof(FollowerEntityAuthoring), true)]
[CanEditMultipleObjects]
public class FollowerEntityAuthoringEditor : EditorBase
{
//bool debug = false;
bool tagPenaltiesOpen;
protected override void OnDisable()
{
//base.OnDisable();
EditorPrefs.SetBool("FollowerEntityAuthoring.tagPenaltiesOpen", tagPenaltiesOpen);
}
protected override void OnEnable()
{
//base.OnEnable();
tagPenaltiesOpen = EditorPrefs.GetBool("FollowerEntityAuthoring.tagPenaltiesOpen", false);
}
public override bool RequiresConstantRepaint()
{
// When the debug inspector is open we want to update it every frame
// as the agent can move
return Application.isPlaying;
}
protected void AutoRepathInspector()
{
var mode = FindProperty("autoRepathBacking.mode");
PropertyField(mode, "Recalculate Paths Automatically");
if (!mode.hasMultipleDifferentValues)
{
var modeValue = (AutoRepathPolicy.Mode)mode.enumValueIndex;
EditorGUI.indentLevel++;
var period = FindProperty("autoRepathBacking.period");
if (modeValue == AutoRepathPolicy.Mode.EveryNSeconds || modeValue == AutoRepathPolicy.Mode.Dynamic)
{
FloatField(period, min: 0f);
}
if (modeValue == AutoRepathPolicy.Mode.Dynamic)
{
EditorGUILayout.HelpBox("The path will be recalculated at least every " + period.floatValue.ToString("0.0") + " seconds, but more often if the destination changes quickly", MessageType.None);
}
EditorGUI.indentLevel--;
}
}
void PathfindingSettingsInspector()
{
bool anyCustomTraversalProvider = this.targets.Any(s => (s as FollowerEntityAuthoring).managedState.pathfindingSettings.traversalProvider != null);
if (anyCustomTraversalProvider)
{
EditorGUILayout.HelpBox("Custom traversal provider active", MessageType.None);
}
PropertyField("managedState.pathfindingSettings.graphMask", "Traversable Graphs");
tagPenaltiesOpen = EditorGUILayout.Foldout(tagPenaltiesOpen, new GUIContent("Tags", "Settings for each tag"));
if (tagPenaltiesOpen)
{
EditorGUI.indentLevel++;
var traversableTags = this.targets.Select(s => (s as FollowerEntityAuthoring).managedState.pathfindingSettings.traversableTags).ToArray();
SeekerEditor.TagsEditor(FindProperty("managedState.pathfindingSettings.tagPenalties"), traversableTags);
for (int i = 0; i < targets.Length; i++)
{
(targets[i] as FollowerEntityAuthoring).managedState.pathfindingSettings.traversableTags = traversableTags[i];
}
EditorGUI.indentLevel--;
}
}
protected override void Inspector()
{
Undo.RecordObjects(targets, "Modify FollowerEntityAuthoring settings");
EditorGUI.BeginChangeCheck();
Section("Shape");
FloatField("shape.radius", min: 0.01f);
FloatField("shape.height", min: 0.01f);
Popup("orientationBacking", new[] { new GUIContent("ZAxisForward (for 3D games)"), new GUIContent("YAxisForward (for 2D games)") }, "Orientation");
Section("Movement");
FloatField("movement.follower.speed", min: 0f);
FloatField("movement.follower.rotationSpeed", min: 0f);
var maxRotationSpeed = FindProperty("movement.follower.rotationSpeed");
FloatField("movement.follower.maxRotationSpeed", min: maxRotationSpeed.hasMultipleDifferentValues ? 0f : maxRotationSpeed.floatValue);
if (ByteAsToggle("movement.follower.allowRotatingOnSpotBacking", "Allow Rotating On The Spot"))
{
EditorGUI.indentLevel++;
FloatField("movement.follower.maxOnSpotRotationSpeed", min: 0f);
FloatField("movement.follower.slowdownTimeWhenTurningOnSpot", min: 0f);
EditorGUI.indentLevel--;
}
Slider("movement.positionSmoothing", left: 0f, right: 0.5f);
Slider("movement.rotationSmoothing", left: 0f, right: 0.5f);
FloatField("movement.follower.slowdownTime", min: 0f);
FloatField("movement.stopDistance", min: 0f);
FloatField("movement.follower.leadInRadiusWhenApproachingDestination", min: 0f);
FloatField("movement.follower.desiredWallDistance", min: 0f);
if (PropertyField("managedState.enableGravity", "Gravity"))
{
EditorGUI.indentLevel++;
PropertyField("movement.groundMask", "Raycast Ground Mask");
EditorGUI.indentLevel--;
}
var movementPlaneSource = FindProperty("movementPlaneSourceBacking");
PropertyField(movementPlaneSource, "Movement Plane Source");
if (AstarPath.active != null && AstarPath.active.data.graphs != null)
{
var possiblySpherical = AstarPath.active.data.navmesh != null && !AstarPath.active.data.navmesh.RecalculateNormals;
if (!possiblySpherical && !movementPlaneSource.hasMultipleDifferentValues && (MovementPlaneSource)movementPlaneSource.intValue == MovementPlaneSource.Raycast)
{
EditorGUILayout.HelpBox("Using raycasts as the movement plane source is only recommended if you have a spherical or otherwise non-planar world. It has a performance overhead.", MessageType.Info);
}
if (!possiblySpherical && !movementPlaneSource.hasMultipleDifferentValues && (MovementPlaneSource)movementPlaneSource.intValue == MovementPlaneSource.NavmeshNormal)
{
EditorGUILayout.HelpBox("Using the navmesh normal as the movement plane source is only recommended if you have a spherical or otherwise non-planar world. It has a performance overhead.", MessageType.Info);
}
}
Section("Pathfinding");
PathfindingSettingsInspector();
AutoRepathInspector();
if (SectionEnableable("Local Avoidance", "managedState.enableLocalAvoidance"))
{
if (Application.isPlaying && RVOSimulator.active == null && !EditorUtility.IsPersistent(target))
{
EditorGUILayout.HelpBox("There is no enabled RVOSimulator component in the scene. A single global RVOSimulator component is required for local avoidance.", MessageType.Warning);
}
FloatField("managedState.rvoSettings.agentTimeHorizon", min: 0f, max: 20.0f);
FloatField("managedState.rvoSettings.obstacleTimeHorizon", min: 0f, max: 20.0f);
PropertyField("managedState.rvoSettings.maxNeighbours");
ClampInt("managedState.rvoSettings.maxNeighbours", min: 0, max: SimulatorBurst.MaxNeighbourCount);
PropertyField("managedState.rvoSettings.layer");
PropertyField("managedState.rvoSettings.collidesWith");
Slider("managedState.rvoSettings.priority", left: 0f, right: 1.0f);
PropertyField("managedState.rvoSettings.locked");
}
Section("Debug");
PropertyField("movement.debugFlags", "Movement Debug Rendering");
PropertyField("managedState.rvoSettings.debug", "Local Avoidance Debug Rendering");
//DebugInspector();
/*if (EditorGUI.EndChangeCheck())
{
for (int i = 0; i < targets.Length; i++)
{
var script = targets[i] as FollowerEntityAuthoring;
script.SyncWithEntity();
}
}*/
}
}
}
Hey there. Aron pointed me at this post in response to my question about RVO in an ECS: RVO + Pathfinding in ECS - #2 by aron_granberg
This looks promising but I’m struggling to follow what you’re doing without the component definitions. Is there any chance you could post them so I can try to reconstruct what you’re doing here in my ECS?
The components are all included in the A* package. AstarPathfindingProject/Core/ECS/Components
.
Oh awesome, thanks!
This script is just a pure Entity version of FollowerEntity. So instead of normal Gameobject prefabs you would add this to an Entity prefab that you instantiate with EntityManager or have placed in a subscene.
RVO seems to work for me both with FollowerEntity and this FollowerEntityAuthoring. Just have to enable LocalAvoidance and add an RVOSimulator to your scene.