using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Assertions; using Unity.MLAgents.Sensors; using UnityEngine.Profiling; namespace Unity.MLAgents.Extensions.Sensors { /// /// Grid-based sensor. /// [AddComponentMenu("ML Agents/Grid Sensor", (int)MenuGroup.Sensors)] public class GridSensor : SensorComponent, ISensor, IBuiltInSensor { /// /// Name of this grid sensor. /// public string Name; // // Main Parameters // /// /// The width of each grid cell. /// [Header("Grid Sensor Settings")] [Tooltip("The width of each grid cell")] [Range(0.05f, 1000f)] public float CellScaleX = 1f; /// /// The depth of each grid cell. /// [Tooltip("The depth of each grid cell")] [Range(0.05f, 1000f)] public float CellScaleZ = 1f; /// /// The width of the grid . /// [Tooltip("The width of the grid")] [Range(2, 2000)] public int GridNumSideX = 16; /// /// The depth of the grid . /// [Tooltip("The depth of the grid")] [Range(2, 2000)] public int GridNumSideZ = 16; /// /// The height of each grid cell. Changes how much of the vertical axis is observed by a cell. /// [Tooltip("The height of each grid cell. Changes how much of the vertical axis is observed by a cell")] [Range(0.01f, 1000f)] public float CellScaleY = 0.01f; /// /// Rotate the grid based on the direction the agent is facing. /// [Tooltip("Rotate the grid based on the direction the agent is facing")] public bool RotateToAgent; /// /// Array holding the depth of each channel. /// [Tooltip("Array holding the depth of each channel")] public int[] ChannelDepth; /// /// List of tags that are detected. /// [Tooltip("List of tags that are detected")] public string[] DetectableObjects; /// /// The layer mask. /// [Tooltip("The layer mask")] public LayerMask ObserveMask; /// /// Enum describing what kind of depth type the data should be organized as /// public enum GridDepthType { Channel, ChannelHot }; /// /// The data layout that the grid should output. /// [Tooltip("The data layout that the grid should output")] public GridDepthType gridDepthType = GridDepthType.Channel; /// /// The reference of the root of the agent. This is used to disambiguate objects with the same tag as the agent. Defaults to current GameObject. /// [Tooltip("The reference of the root of the agent. This is used to disambiguate objects with the same tag as the agent. Defaults to current GameObject")] public GameObject rootReference; [Header("Collider Buffer Properties")] [Tooltip("The absolute max size of the Collider buffer used in the non-allocating Physics calls. In other words" + " the Collider buffer will never grow beyond this number even if there are more Colliders in the Grid Cell.")] public int MaxColliderBufferSize = 500; [Tooltip( "The Estimated Max Number of Colliders to expect per cell. This number is used to " + "pre-allocate an array of Colliders in order to take advantage of the OverlapBoxNonAlloc " + "Physics API. If the number of colliders found is >= InitialColliderBufferSize the array " + "will be resized to double its current size. The hard coded absolute size is 500.")] public int InitialColliderBufferSize = 4; Collider[] m_ColliderBuffer; float[] m_ChannelBuffer; // // Hidden Parameters // /// /// The total number of observations per cell of the grid. Its equivalent to the "channel" on the outgoing tensor. /// [HideInInspector] public int ObservationPerCell; /// /// The total number of observations that this GridSensor provides. It is the length of m_PerceptionBuffer. /// [HideInInspector] public int NumberOfObservations; /// /// The offsets used to specify where within a cell's allotted data, certain observations will be inserted. /// [HideInInspector] public int[] ChannelOffsets; /// /// The main storage of perceptual information. /// protected float[] m_PerceptionBuffer; /// /// The default value of the perceptionBuffer when using the ChannelHot DepthType. Used to reset the array/ /// protected float[] m_ChannelHotDefaultPerceptionBuffer; /// /// Array of Colors needed in order to load the values of the perception buffer to a texture. /// protected Color[] m_PerceptionColors; /// /// Texture where the colors are written to so that they can be compressed in PNG format. /// protected Texture2D m_perceptionTexture2D; // // Utility Constants Calculated on Init // /// /// Number of PNG formated images that are sent to python during training. /// private int NumImages; /// /// Number of relevant channels on the last image that is sent/ /// private int NumChannelsOnLastImage; /// /// Radius of grid, used for normalizing the distance. /// protected float InverseSphereRadius; /// /// Total Number of cells (width*height) /// private int NumCells; /// /// Difference between GridNumSideZ and gridNumSideX. /// protected int DiffNumSideZX = 0; /// /// Offset used for calculating CellToPoint /// protected float OffsetGridNumSide = 7.5f; // (gridNumSideZ - 1) / 2; /// /// Half of the grid in the X direction /// private float HalfOfGridX; /// /// Half of the grid in the z direction /// private float HalfOfGridZ; /// /// Used in the PointToCell method to scale the x value to land in the calculated cell. /// private float PointToCellScalingX; /// /// Used in the PointToCell method to scale the y value to land in the calculated cell. /// private float PointToCellScalingZ; /// /// Bool if initialized or not. /// protected bool Initialized = false; /// /// Cached ObservationSpec /// private ObservationSpec m_ObservationSpec; // // Debug Parameters // /// /// Array of Colors used for the grid gizmos. /// [Header("Debug Options")] [Tooltip("Array of Colors used for the grid gizmos")] public Color[] DebugColors; /// /// The height of the gizmos grid. /// [Tooltip("The height of the gizmos grid")] public float GizmoYOffset = 0f; /// /// Whether to show gizmos or not. /// [Tooltip("Whether to show gizmos or not")] public bool ShowGizmos = false; public SensorCompressionType CompressionType = SensorCompressionType.PNG; /// /// Array of colors displaying the DebugColors for each cell in OnDrawGizmos. Only updated if ShowGizmos. /// protected Color[] CellActivity; /// /// Array of positions where each position is the center of a cell. /// private Vector3[] CellPoints; /// /// List representing the multiple compressed images of all of the grids /// private List compressedImgs; /// /// List representing the sizes of the multiple images so they can be properly reconstructed on the python side /// private List byteSizesBytesList; private Color DebugDefaultColor = new Color(1f, 1f, 1f, 0.25f); /// public override ISensor[] CreateSensors() { return new ISensor[] { this }; } /// /// Sets the parameters of the grid sensor /// /// array of strings representing the tags to be detected by the sensor /// array of ints representing the depth of each channel /// enum representing the GridDepthType of the sensor /// float representing the X scaling of each cell /// float representing the Z scaling of each cell /// int representing the number of cells in the X direction. Width of the Grid /// int representing the number of cells in the Z direction. Height of the Grid /// int representing the layer mask to observe /// bool if true then the grid is rotated to the rotation of the transform the rootReference /// array of colors corresponding the the tags in the detectableObjects array public virtual void SetParameters(string[] detectableObjects, int[] channelDepth, GridDepthType gridDepthType, float cellScaleX, float cellScaleZ, int gridWidth, int gridHeight, int observeMaskInt, bool rotateToAgent, Color[] debugColors) { this.ObserveMask = observeMaskInt; this.DetectableObjects = detectableObjects; this.ChannelDepth = channelDepth; this.gridDepthType = gridDepthType; this.CellScaleX = cellScaleX; this.CellScaleZ = cellScaleZ; this.GridNumSideX = gridWidth; this.GridNumSideZ = gridHeight; this.RotateToAgent = rotateToAgent; this.DiffNumSideZX = (GridNumSideZ - GridNumSideX); this.OffsetGridNumSide = (GridNumSideZ - 1f) / 2f; this.DebugColors = debugColors; } /// /// Initializes the constant parameters used within the perceive method call /// public void InitGridParameters() { NumCells = GridNumSideX * GridNumSideZ; float sphereRadiusX = (CellScaleX * GridNumSideX) / Mathf.Sqrt(2); float sphereRadiusZ = (CellScaleZ * GridNumSideZ) / Mathf.Sqrt(2); InverseSphereRadius = 1.0f / Mathf.Max(sphereRadiusX, sphereRadiusZ); ChannelOffsets = new int[ChannelDepth.Length]; DiffNumSideZX = (GridNumSideZ - GridNumSideX); OffsetGridNumSide = (GridNumSideZ - 1f) / 2f; HalfOfGridX = CellScaleX * GridNumSideX / 2; HalfOfGridZ = CellScaleZ * GridNumSideZ / 2; PointToCellScalingX = GridNumSideX / (CellScaleX * GridNumSideX); PointToCellScalingZ = GridNumSideZ / (CellScaleZ * GridNumSideZ); } /// /// Initializes the constant parameters that are based on the Grid Depth Type /// Sets the ObservationPerCell and the ChannelOffsets properties /// public virtual void InitDepthType() { switch (gridDepthType) { case GridDepthType.Channel: ObservationPerCell = ChannelDepth.Length; break; case GridDepthType.ChannelHot: ObservationPerCell = 0; ChannelOffsets[ChannelOffsets.Length - 1] = 0; for (int i = 1; i < ChannelDepth.Length; i++) { ChannelOffsets[i] = ChannelOffsets[i - 1] + ChannelDepth[i - 1]; } for (int i = 0; i < ChannelDepth.Length; i++) { ObservationPerCell += ChannelDepth[i]; } break; } // The maximum number of channels in the final output must be less than 255 * 3 because the "number of PNG images" to generate must fit in one byte Assert.IsTrue(ObservationPerCell < (255 * 3), "The maximum number of channels per cell must be less than 255 * 3"); } /// /// Initializes the location of the CellPoints property /// private void InitCellPoints() { CellPoints = new Vector3[NumCells]; for (int i = 0; i < NumCells; i++) { CellPoints[i] = CellToPoint(i, false); } } /// /// Initializes the m_ChannelHotDefaultPerceptionBuffer with default data in the case that the grid depth type is ChannelHot /// public virtual void InitChannelHotDefaultPerceptionBuffer() { m_ChannelHotDefaultPerceptionBuffer = new float[ObservationPerCell]; for (int i = 0; i < ChannelDepth.Length; i++) { if (ChannelDepth[i] > 1) { m_ChannelHotDefaultPerceptionBuffer[ChannelOffsets[i]] = 1; } } } /// /// Initializes the m_PerceptionBuffer as the main data storage property /// Calculates the NumImages and NumChannelsOnLastImage that are used for serializing m_PerceptionBuffer /// public void InitPerceptionBuffer() { if (Application.isPlaying) Initialized = true; NumberOfObservations = ObservationPerCell * NumCells; m_PerceptionBuffer = new float[NumberOfObservations]; if (gridDepthType == GridDepthType.ChannelHot) { InitChannelHotDefaultPerceptionBuffer(); } m_PerceptionColors = new Color[NumCells]; NumImages = ObservationPerCell / 3; NumChannelsOnLastImage = ObservationPerCell % 3; if (NumChannelsOnLastImage == 0) NumChannelsOnLastImage = 3; else NumImages += 1; CellActivity = new Color[NumCells]; } /// /// Calls the initialization methods. Creates the data storing properties used to send the data /// Establishes /// public virtual void Start() { InitGridParameters(); InitDepthType(); InitCellPoints(); InitPerceptionBuffer(); m_ColliderBuffer = new Collider[Math.Min(MaxColliderBufferSize, InitialColliderBufferSize)]; // Default root reference to current game object if (rootReference == null) rootReference = gameObject; m_ObservationSpec = ObservationSpec.Visual(GridNumSideX, GridNumSideZ, ObservationPerCell); compressedImgs = new List(); byteSizesBytesList = new List(); m_perceptionTexture2D = new Texture2D(GridNumSideX, GridNumSideZ, TextureFormat.RGB24, false); } /// void ISensor.Reset() { } /// /// Clears the perception buffer before loading in new data. If the gridDepthType is ChannelHot, then it initializes the /// Reset() also reinits the cell activity array (for debug) /// public void ClearPerceptionBuffer() { if (m_PerceptionBuffer != null) { if (gridDepthType == GridDepthType.ChannelHot) { // Copy the default value to the array for (int i = 0; i < NumCells; i++) { Array.Copy(m_ChannelHotDefaultPerceptionBuffer, 0, m_PerceptionBuffer, i * ObservationPerCell, ObservationPerCell); } } else { Array.Clear(m_PerceptionBuffer, 0, m_PerceptionBuffer.Length); } } else { m_PerceptionBuffer = new float[NumberOfObservations]; m_ColliderBuffer = new Collider[Math.Min(MaxColliderBufferSize, InitialColliderBufferSize)]; } if (ShowGizmos) { // Ensure to init arrays if not yet assigned (for editor) if (CellActivity == null) CellActivity = new Color[NumCells]; // Assign the default color to the cell activities for (int i = 0; i < NumCells; i++) { CellActivity[i] = DebugDefaultColor; } } } /// public string GetName() { return Name; } /// public virtual CompressionSpec GetCompressionSpec() { return new CompressionSpec(CompressionType); } /// public BuiltInSensorType GetBuiltInSensorType() { return BuiltInSensorType.GridSensor; } /// /// GetCompressedObservation - Calls Perceive then puts the data stored on the perception buffer /// onto the m_perceptionTexture2D to be converted to a byte array and returned /// /// byte[] containing the compressed observation of the grid observation public byte[] GetCompressedObservation() { using (TimerStack.Instance.Scoped("GridSensor.GetCompressedObservation")) { Perceive(); // Fill the perception buffer with observed data var allBytes = new List(); for (int i = 0; i < NumImages - 1; i++) { ChannelsToTexture(3 * i, 3); allBytes.AddRange(m_perceptionTexture2D.EncodeToPNG()); } ChannelsToTexture(3 * (NumImages - 1), NumChannelsOnLastImage); allBytes.AddRange(m_perceptionTexture2D.EncodeToPNG()); return allBytes.ToArray(); } } /// /// ChannelsToTexture - Takes the channel index and the numChannelsToAdd. /// For each cell and for each channel to add, sets it to a value of the color specified for that cell. /// All colors are then set to the perceptionTexture via SetPixels. /// m_perceptionTexture2D can then be read as an image as it now contains all of the information that was /// stored in the channels /// /// /// protected void ChannelsToTexture(int channelIndex, int numChannelsToAdd) { for (int i = 0; i < NumCells; i++) { for (int j = 0; j < numChannelsToAdd; j++) { m_PerceptionColors[i][j] = m_PerceptionBuffer[i * ObservationPerCell + channelIndex + j]; } } m_perceptionTexture2D.SetPixels(m_PerceptionColors); } /// /// Perceive - Clears the buffers, calls overlap box on the actual cell (the actual perception part) /// for all found colliders, LoadObjectData is called /// at the end, Perceive returns the float array of the perceptions /// /// A float[] containing all of the information collected from the gridsensor public float[] Perceive() { if (m_ColliderBuffer == null) { return Array.Empty(); } ClearPerceptionBuffer(); using (TimerStack.Instance.Scoped("GridSensor.Perceive")) { var halfCellScale = new Vector3(CellScaleX / 2f, CellScaleY, CellScaleZ / 2f); for (var cellIndex = 0; cellIndex < NumCells; cellIndex++) { int numFound; Vector3 cellCenter; if (RotateToAgent) { Transform transform1; cellCenter = (transform1 = transform).TransformPoint(CellPoints[cellIndex]); numFound = BufferResizingOverlapBoxNonAlloc(cellCenter, halfCellScale, transform1.rotation); } else { cellCenter = transform.position + CellPoints[cellIndex]; numFound = BufferResizingOverlapBoxNonAlloc(cellCenter, halfCellScale, Quaternion.identity); } if (numFound > 0) { ParseColliders(m_ColliderBuffer, numFound, cellIndex, cellCenter); } } } return m_PerceptionBuffer; } /// /// This method attempts to perform the Physics.OverlapBoxNonAlloc and will double the size of the Collider buffer /// if the number of Colliders in the buffer after the call is equal to the length of the buffer. /// /// /// /// /// int BufferResizingOverlapBoxNonAlloc(Vector3 cellCenter, Vector3 halfCellScale, Quaternion rotation) { int numFound; // Since we can only get a fixed number of results, requery // until we're sure we can hold them all (or until we hit the max size). while (true) { numFound = Physics.OverlapBoxNonAlloc(cellCenter, halfCellScale, m_ColliderBuffer, rotation, ObserveMask); if (numFound == m_ColliderBuffer.Length && m_ColliderBuffer.Length < MaxColliderBufferSize) { m_ColliderBuffer = new Collider[Math.Min(MaxColliderBufferSize, m_ColliderBuffer.Length * 2)]; InitialColliderBufferSize = m_ColliderBuffer.Length; } else { break; } } return numFound; } /// /// Parses the array of colliders found within a cell. Finds the closest gameobject to the agent root reference within the cell /// /// Array of the colliders found within the cell /// Number of colliders found. /// The index of the cell /// The center position of the cell protected virtual void ParseColliders(Collider[] foundColliders, int numFound, int cellIndex, Vector3 cellCenter) { Profiler.BeginSample("GridSensor.ParseColliders"); GameObject closestColliderGo = null; var minDistanceSquared = float.MaxValue; for (var i = 0; i < numFound; i++) { var currentColliderGo = foundColliders[i].gameObject; // Continue if the current collider go is the root reference if (ReferenceEquals(currentColliderGo, rootReference)) continue; var closestColliderPoint = foundColliders[i].ClosestPointOnBounds(cellCenter); var currentDistanceSquared = (closestColliderPoint - rootReference.transform.position).sqrMagnitude; // Checks if our colliders contain a detectable object var index = -1; for (var ii = 0; ii < DetectableObjects.Length; ii++) { if (currentColliderGo.CompareTag(DetectableObjects[ii])) { index = ii; break; } } if (index > -1 && currentDistanceSquared < minDistanceSquared) { minDistanceSquared = currentDistanceSquared; closestColliderGo = currentColliderGo; } } if (!ReferenceEquals(closestColliderGo, null)) LoadObjectData(closestColliderGo, cellIndex, (float)Math.Sqrt(minDistanceSquared) * InverseSphereRadius); Profiler.EndSample(); } /// /// GetObjectData - returns an array of values that represent the game object /// This is one of the few methods that one may need to override to get their required functionality /// For instance, if one wants specific information about the current gameobject, they can use this method /// to extract it and then return it in an array format. /// /// /// A float[] containing the data that holds the representative information of the passed in gameObject /// /// The game object that was found colliding with a certain cell /// The index of the type (tag) of the gameObject. /// (e.g., if this GameObject had the 3rd tag out of 4, type_index would be 2.0f) /// A float between 0 and 1 describing the ratio of /// the distance currentColliderGo is compared to the edge of the gridsensor /// /// Here is an example of extenind GetObjectData to include information about a potential Rigidbody: /// /// protected override float[] GetObjectData(GameObject currentColliderGo, /// float type_index, float normalized_distance) /// { /// float[] channelValues = new float[ChannelDepth.Length]; // ChannelDepth.Length = 4 in this example /// channelValues[0] = type_index; /// Rigidbody goRb = currentColliderGo.GetComponent<Rigidbody>(); /// if (goRb != null) /// { /// channelValues[1] = goRb.velocity.x; /// channelValues[2] = goRb.velocity.y; /// channelValues[3] = goRb.velocity.z; /// } /// return channelValues; /// } /// /// protected virtual float[] GetObjectData(GameObject currentColliderGo, float typeIndex, float normalizedDistance) { if (m_ChannelBuffer == null) { m_ChannelBuffer = new float[ChannelDepth.Length]; } Array.Clear(m_ChannelBuffer, 0, m_ChannelBuffer.Length); m_ChannelBuffer[0] = typeIndex; return m_ChannelBuffer; } /// /// Runs basic validation assertions to check that the values can be normalized /// /// The values to be validated /// The gameobject used for better error messages protected virtual void ValidateValues(float[] channelValues, GameObject currentColliderGo) { for (int j = 0; j < channelValues.Length; j++) { if (channelValues[j] < 0) throw new UnityAgentsException("Expected ChannelValue[" + j + "] for " + currentColliderGo.name + " to be non-negative, was " + channelValues[j]); if (channelValues[j] > ChannelDepth[j]) throw new UnityAgentsException("Expected ChannelValue[" + j + "] for " + currentColliderGo.name + " to be less than ChannelDepth[" + j + "] (" + ChannelDepth[j] + "), was " + channelValues[j]); } } /// /// LoadObjectData - If the GameObject matches a tag, GetObjectData is called to extract the data from the GameObject /// then the data is transformed based on the GridDepthType of the gridsensor. /// Further documetation on the GridDepthType can be found below /// /// The game object that was found colliding with a certain cell /// The index of the current cell /// A float between 0 and 1 describing the ratio of /// the distance currentColliderGo is compared to the edge of the gridsensor protected virtual void LoadObjectData(GameObject currentColliderGo, int cellIndex, float normalizedDistance) { Profiler.BeginSample("GridSensor.LoadObjectData"); var channelHotVals = new ArraySegment(m_PerceptionBuffer, cellIndex * ObservationPerCell, ObservationPerCell); for (var i = 0; i < DetectableObjects.Length; i++) { for (var ii = 0; ii < channelHotVals.Count; ii++) { m_PerceptionBuffer[channelHotVals.Offset + ii] = 0f; } if (!ReferenceEquals(currentColliderGo, null) && currentColliderGo.CompareTag(DetectableObjects[i])) { // TODO: Create the array already then set the values using "out" in GetObjectData // Using i+1 as the type index as "0" represents "empty" float[] channelValues = GetObjectData(currentColliderGo, (float)i + 1, normalizedDistance); ValidateValues(channelValues, currentColliderGo); if (ShowGizmos) { Color debugRayColor = Color.white; if (DebugColors.Length > 0) { debugRayColor = DebugColors[i]; } CellActivity[cellIndex] = new Color(debugRayColor.r, debugRayColor.g, debugRayColor.b, .5f); } switch (gridDepthType) { case GridDepthType.Channel: { // The observations are "channel based" so each grid is WxHxC where C is the number of channels // This typically means that each channel value is normalized between 0 and 1 // If channelDepth is 1, the value is assumed normalized, else the value is normalized by the channelDepth // The channels are then stored consecutively in PerceptionBuffer. // NOTE: This is the only grid type that uses floating point values // For example, if a cell contains the 3rd type of 5 possible on the 2nd team of 3 possible teams: // channelValues = {2, 1} // ObservationPerCell = channelValues.Length // channelValues = {2f/5f, 1f/3f} = {.4, .33..} // Array.Copy(channelValues, 0, PerceptionBuffer, cell_id*ObservationPerCell, ObservationPerCell); for (int j = 0; j < channelValues.Length; j++) { channelValues[j] /= ChannelDepth[j]; } Array.Copy(channelValues, 0, m_PerceptionBuffer, cellIndex * ObservationPerCell, ObservationPerCell); break; } case GridDepthType.ChannelHot: { // The observations are "channel hot" so each grid is WxHxD where D is the sum of all of the channel depths // The opposite of the "channel based" case, the channel values are represented as one hot vector per channel and then concatenated together // Thus channelDepth is assumed to be greater than 1. // For example, if a cell contains the 3rd type of 5 possible on the 2nd team of 3 possible teams, // channelValues = {2, 1} // channelOffsets = {5, 3} // ObservationPerCell = 5 + 3 = 8 // channelHotVals = {0, 0, 1, 0, 0, 0, 1, 0} // Array.Copy(channelHotVals, 0, PerceptionBuffer, cell_id*ObservationPerCell, ObservationPerCell); for (int j = 0; j < channelValues.Length; j++) { if (ChannelDepth[j] > 1) { m_PerceptionBuffer[channelHotVals.Offset + (int)channelValues[j] + ChannelOffsets[j]] = 1f; } else { m_PerceptionBuffer[channelHotVals.Offset + ChannelOffsets[j]] = channelValues[j]; } } break; } } break; } } Profiler.EndSample(); } /// Converts the index of the cell to the 3D point (y is zero) /// Vector3 of the position of the center of the cell /// The index of the cell /// Bool weather to transform the point to the current transform protected Vector3 CellToPoint(int cell, bool shouldTransformPoint = true) { float x = (cell % GridNumSideZ - OffsetGridNumSide) * CellScaleX; float z = (cell / GridNumSideZ - OffsetGridNumSide) * CellScaleZ - DiffNumSideZX; if (shouldTransformPoint) return transform.TransformPoint(new Vector3(x, 0, z)); return new Vector3(x, 0, z); } /// Finds the cell in which the given global point falls /// /// The index of the cell in which the global point falls or -1 if the point does not fall into a cell /// /// The 3D point in global space public int PointToCell(Vector3 globalPoint) { Vector3 point = transform.InverseTransformPoint(globalPoint); if (point.x < -HalfOfGridX || point.x > HalfOfGridX || point.z < -HalfOfGridZ || point.z > HalfOfGridZ) return -1; float x = point.x + HalfOfGridX; float z = point.z + HalfOfGridZ; int _x = (int)Mathf.Floor(x * PointToCellScalingX); int _z = (int)Mathf.Floor(z * PointToCellScalingZ); return GridNumSideX * _z + _x; } /// Copies the data from one cell to another /// index of the cell to copy from /// index of the cell to copy into protected void CopyCellData(int fromCellID, int toCellID) { Array.Copy(m_PerceptionBuffer, fromCellID * ObservationPerCell, m_PerceptionBuffer, toCellID * ObservationPerCell, ObservationPerCell); if (ShowGizmos) CellActivity[toCellID] = CellActivity[fromCellID]; } void OnDrawGizmos() { if (ShowGizmos) { if (Application.isEditor && !Application.isPlaying) Start(); Perceive(); var scale = new Vector3(CellScaleX, 1, CellScaleZ); var offset = new Vector3(0, GizmoYOffset, 0); var oldGizmoMatrix = Gizmos.matrix; for (var i = 0; i < NumCells; i++) { Matrix4x4 cubeTransform; if (RotateToAgent) { cubeTransform = Matrix4x4.TRS(CellToPoint(i) + offset, transform.rotation, scale); } else { cubeTransform = Matrix4x4.TRS(CellToPoint(i, false) + transform.position + offset, Quaternion.identity, scale); } Gizmos.matrix = oldGizmoMatrix * cubeTransform; Gizmos.color = CellActivity[i]; Gizmos.DrawCube(Vector3.zero, Vector3.one); } Gizmos.matrix = oldGizmoMatrix; if (Application.isEditor && !Application.isPlaying) DestroyImmediate(m_perceptionTexture2D); } } /// void ISensor.Update() { using (TimerStack.Instance.Scoped("GridSensor.Update")) { Perceive(); } } /// Gets the observation shape /// int[] of the observation shape public ObservationSpec GetObservationSpec() { // Lazy update var shape = m_ObservationSpec.Shape; if (shape[0] != GridNumSideX || shape[1] != GridNumSideZ || shape[2] != ObservationPerCell) { m_ObservationSpec = ObservationSpec.Visual(GridNumSideX, GridNumSideZ, ObservationPerCell); } return m_ObservationSpec; } /// public int Write(ObservationWriter writer) { using (TimerStack.Instance.Scoped("GridSensor.WriteToTensor")) { int index = 0; for (var h = GridNumSideZ - 1; h >= 0; h--) // height { for (var w = 0; w < GridNumSideX; w++) // width { for (var d = 0; d < ObservationPerCell; d++) // depth { writer[h, w, d] = m_PerceptionBuffer[index]; index++; } } } return index; } } } }