using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Serialization; namespace Unity.MLAgents.Sensors { /// /// A base class to support sensor components for raycast-based sensors. /// public abstract class RayPerceptionSensorComponentBase : SensorComponent { [HideInInspector, SerializeField, FormerlySerializedAs("sensorName")] string m_SensorName = "RayPerceptionSensor"; /// /// The name of the Sensor that this component wraps. /// Note that changing this at runtime does not affect how the Agent sorts the sensors. /// public string SensorName { get { return m_SensorName; } set { m_SensorName = value; } } [SerializeField, FormerlySerializedAs("detectableTags")] [Tooltip("List of tags in the scene to compare against.")] List m_DetectableTags; /// /// List of tags in the scene to compare against. /// Note that this should not be changed at runtime. /// public List DetectableTags { get { return m_DetectableTags; } set { m_DetectableTags = value; } } [HideInInspector, SerializeField, FormerlySerializedAs("raysPerDirection")] [Range(0, 50)] [Tooltip("Number of rays to the left and right of center.")] int m_RaysPerDirection = 3; /// /// Number of rays to the left and right of center. /// Note that this should not be changed at runtime. /// public int RaysPerDirection { get { return m_RaysPerDirection; } // Note: can't change at runtime set { m_RaysPerDirection = value;} } [HideInInspector, SerializeField, FormerlySerializedAs("maxRayDegrees")] [Range(0, 180)] [Tooltip("Cone size for rays. Using 90 degrees will cast rays to the left and right. " + "Greater than 90 degrees will go backwards.")] float m_MaxRayDegrees = 70; /// /// Cone size for rays. Using 90 degrees will cast rays to the left and right. /// Greater than 90 degrees will go backwards. /// public float MaxRayDegrees { get => m_MaxRayDegrees; set { m_MaxRayDegrees = value; UpdateSensor(); } } [HideInInspector, SerializeField, FormerlySerializedAs("sphereCastRadius")] [Range(0f, 10f)] [Tooltip("Radius of sphere to cast. Set to zero for raycasts.")] float m_SphereCastRadius = 0.5f; /// /// Radius of sphere to cast. Set to zero for raycasts. /// public float SphereCastRadius { get => m_SphereCastRadius; set { m_SphereCastRadius = value; UpdateSensor(); } } [HideInInspector, SerializeField, FormerlySerializedAs("rayLength")] [Range(1, 1000)] [Tooltip("Length of the rays to cast.")] float m_RayLength = 20f; /// /// Length of the rays to cast. /// public float RayLength { get => m_RayLength; set { m_RayLength = value; UpdateSensor(); } } [HideInInspector, SerializeField, FormerlySerializedAs("rayLayerMask")] [Tooltip("Controls which layers the rays can hit.")] LayerMask m_RayLayerMask = Physics.DefaultRaycastLayers; /// /// Controls which layers the rays can hit. /// public LayerMask RayLayerMask { get => m_RayLayerMask; set { m_RayLayerMask = value; UpdateSensor(); } } [HideInInspector, SerializeField, FormerlySerializedAs("observationStacks")] [Range(1, 50)] [Tooltip("Number of raycast results that will be stacked before being fed to the neural network.")] int m_ObservationStacks = 1; /// /// Whether to stack previous observations. Using 1 means no previous observations. /// Note that changing this after the sensor is created has no effect. /// public int ObservationStacks { get { return m_ObservationStacks; } set { m_ObservationStacks = value; } } /// /// Color to code a ray that hits another object. /// [HideInInspector] [SerializeField] [Header("Debug Gizmos", order = 999)] internal Color rayHitColor = Color.red; /// /// Color to code a ray that avoid or misses all other objects. /// [HideInInspector] [SerializeField] internal Color rayMissColor = Color.white; [NonSerialized] RayPerceptionSensor m_RaySensor; /// /// Get the RayPerceptionSensor that was created. /// public RayPerceptionSensor RaySensor { get => m_RaySensor; } /// /// Returns the for the associated raycast sensor. /// /// public abstract RayPerceptionCastType GetCastType(); /// /// Returns the amount that the ray start is offset up or down by. /// /// public virtual float GetStartVerticalOffset() { return 0f; } /// /// Returns the amount that the ray end is offset up or down by. /// /// public virtual float GetEndVerticalOffset() { return 0f; } /// /// Returns an initialized raycast sensor. /// /// public override ISensor CreateSensor() { var rayPerceptionInput = GetRayPerceptionInput(); m_RaySensor = new RayPerceptionSensor(m_SensorName, rayPerceptionInput); if (ObservationStacks != 1) { var stackingSensor = new StackingSensor(m_RaySensor, ObservationStacks); return stackingSensor; } return m_RaySensor; } /// /// Returns the specific ray angles given the number of rays per direction and the /// cone size for the rays. /// /// Number of rays to the left and right of center. /// /// Cone size for rays. Using 90 degrees will cast rays to the left and right. /// Greater than 90 degrees will go backwards. /// /// internal static float[] GetRayAngles(int raysPerDirection, float maxRayDegrees) { // Example: // { 90, 90 - delta, 90 + delta, 90 - 2*delta, 90 + 2*delta } var anglesOut = new float[2 * raysPerDirection + 1]; var delta = maxRayDegrees / raysPerDirection; anglesOut[0] = 90f; for (var i = 0; i < raysPerDirection; i++) { anglesOut[2 * i + 1] = 90 - (i + 1) * delta; anglesOut[2 * i + 2] = 90 + (i + 1) * delta; } return anglesOut; } /// /// Returns the observation shape for this raycast sensor which depends on the number /// of tags for detected objects and the number of rays. /// /// public override int[] GetObservationShape() { var numRays = 2 * RaysPerDirection + 1; var numTags = m_DetectableTags?.Count ?? 0; var obsSize = (numTags + 2) * numRays; var stacks = ObservationStacks > 1 ? ObservationStacks : 1; return new[] { obsSize * stacks }; } /// /// Get the RayPerceptionInput that is used by the . /// /// public RayPerceptionInput GetRayPerceptionInput() { var rayAngles = GetRayAngles(RaysPerDirection, MaxRayDegrees); var rayPerceptionInput = new RayPerceptionInput(); rayPerceptionInput.RayLength = RayLength; rayPerceptionInput.DetectableTags = DetectableTags; rayPerceptionInput.Angles = rayAngles; rayPerceptionInput.StartOffset = GetStartVerticalOffset(); rayPerceptionInput.EndOffset = GetEndVerticalOffset(); rayPerceptionInput.CastRadius = SphereCastRadius; rayPerceptionInput.Transform = transform; rayPerceptionInput.CastType = GetCastType(); rayPerceptionInput.LayerMask = RayLayerMask; return rayPerceptionInput; } internal void UpdateSensor() { if (m_RaySensor != null) { var rayInput = GetRayPerceptionInput(); m_RaySensor.SetRayPerceptionInput(rayInput); } } void OnDrawGizmosSelected() { if (m_RaySensor?.debugDisplayInfo?.rayInfos != null) { // If we have cached debug info from the sensor, draw that. // Draw "old" observations in a lighter color. // Since the agent may not step every frame, this helps de-emphasize "stale" hit information. var alpha = Mathf.Pow(.5f, m_RaySensor.debugDisplayInfo.age); foreach (var rayInfo in m_RaySensor.debugDisplayInfo.rayInfos) { DrawRaycastGizmos(rayInfo, alpha); } } else { var rayInput = GetRayPerceptionInput(); // We don't actually need the tags here, since they don't affect the display of the rays. // Additionally, the user might be in the middle of typing the tag name when this is called, // and there's no way to turn off the "Tag ... is not defined" error logs. // So just don't use any tags here. rayInput.DetectableTags = null; for (var rayIndex = 0; rayIndex < rayInput.Angles.Count; rayIndex++) { DebugDisplayInfo.RayInfo debugRay; RayPerceptionSensor.PerceiveSingleRay(rayInput, rayIndex, out debugRay); DrawRaycastGizmos(debugRay); } } } /// /// Draw the debug information from the sensor (if available). /// void DrawRaycastGizmos(DebugDisplayInfo.RayInfo rayInfo, float alpha = 1.0f) { var startPositionWorld = rayInfo.worldStart; var endPositionWorld = rayInfo.worldEnd; var rayDirection = endPositionWorld - startPositionWorld; rayDirection *= rayInfo.rayOutput.HitFraction; // hit fraction ^2 will shift "far" hits closer to the hit color var lerpT = rayInfo.rayOutput.HitFraction * rayInfo.rayOutput.HitFraction; var color = Color.Lerp(rayHitColor, rayMissColor, lerpT); color.a *= alpha; Gizmos.color = color; Gizmos.DrawRay(startPositionWorld, rayDirection); // Draw the hit point as a sphere. If using rays to cast (0 radius), use a small sphere. if (rayInfo.rayOutput.HasHit) { var hitRadius = Mathf.Max(rayInfo.castRadius, .05f); Gizmos.DrawWireSphere(startPositionWorld + rayDirection, hitRadius); } } } }