GitHub
4 年前
当前提交
ca2f0b78
共有 37 个文件被更改,包括 2560 次插入 和 1315 次删除
-
50Project/Assets/ML-Agents/Examples/FoodCollector/Prefabs/FoodCollectorArea.prefab
-
276Project/Assets/ML-Agents/Examples/PushBlock/Prefabs/PushBlockCollabAreaGrid.prefab
-
172Project/Assets/ML-Agents/Examples/PushBlock/Scenes/PushBlockCollab.unity
-
995Project/Assets/ML-Agents/Examples/PushBlock/TFModels/PushBlockCollab.onnx
-
8Project/Assets/ML-Agents/Examples/PushBlock/TFModels/PushBlockCollab.onnx.meta
-
50com.unity.ml-agents.extensions/Editor/GridSensorComponentEditor.cs
-
158com.unity.ml-agents.extensions/Runtime/Sensors/GridSensorComponent.cs
-
2com.unity.ml-agents.extensions/Runtime/Sensors/BoxOverlapChecker.cs.meta
-
2com.unity.ml-agents.extensions/Tests/Editor/GridSensors/BoxOverlapCheckerTests.cs.meta
-
2com.unity.ml-agents.extensions/Tests/Editor/GridSensors/GridSensorTests.cs.meta
-
2com.unity.ml-agents.extensions/Runtime/Sensors/CountingGridSensor.cs.meta
-
2com.unity.ml-agents.extensions/Runtime/Sensors/GridSensorBase.cs.meta
-
2com.unity.ml-agents.extensions/Runtime/Sensors/OneHotGridSensor.cs.meta
-
2com.unity.ml-agents.extensions/Tests/Editor/GridSensors/SimpleTestGridSensor.cs.meta
-
254com.unity.ml-agents.extensions/Runtime/Sensors/BoxOverlapChecker.cs
-
61com.unity.ml-agents.extensions/Runtime/Sensors/CountingGridSensor.cs
-
366com.unity.ml-agents.extensions/Runtime/Sensors/GridSensorBase.cs
-
60com.unity.ml-agents.extensions/Runtime/Sensors/OneHotGridSensor.cs
-
8com.unity.ml-agents.extensions/Tests/Editor/GridSensors.meta
-
297com.unity.ml-agents.extensions/Tests/Editor/GridSensors/BoxOverlapCheckerTests.cs
-
88com.unity.ml-agents.extensions/Tests/Editor/GridSensors/GridSensorTestUtils.cs
-
210com.unity.ml-agents.extensions/Tests/Editor/GridSensors/GridSensorTests.cs
-
131com.unity.ml-agents.extensions/Tests/Editor/GridSensors/SimpleTestGridSensor.cs
-
661com.unity.ml-agents.extensions/Runtime/Sensors/GridSensor.cs
-
8com.unity.ml-agents.extensions/Tests/Editor/Sensors.meta
-
8com.unity.ml-agents.extensions/Tests/Utils.meta
-
0/com.unity.ml-agents.extensions/Runtime/Sensors/BoxOverlapChecker.cs.meta
-
0/com.unity.ml-agents.extensions/Tests/Editor/GridSensors/BoxOverlapCheckerTests.cs.meta
-
0/com.unity.ml-agents.extensions/Tests/Editor/GridSensors/GridSensorTests.cs.meta
-
0/com.unity.ml-agents.extensions/Runtime/Sensors/CountingGridSensor.cs.meta
-
0/com.unity.ml-agents.extensions/Runtime/Sensors/GridSensorBase.cs.meta
-
0/com.unity.ml-agents.extensions/Runtime/Sensors/OneHotGridSensor.cs.meta
-
0/com.unity.ml-agents.extensions/Tests/Editor/GridSensors/GridSensorTestUtils.cs.meta
-
0/com.unity.ml-agents.extensions/Tests/Editor/GridSensors/SimpleTestGridSensor.cs.meta
995
Project/Assets/ML-Agents/Examples/PushBlock/TFModels/PushBlockCollab.onnx
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using UnityEngine; |
|||
|
|||
[assembly: InternalsVisibleTo("Unity.ML-Agents.Extensions.EditorTests")] |
|||
namespace Unity.MLAgents.Extensions.Sensors |
|||
{ |
|||
internal class BoxOverlapChecker |
|||
{ |
|||
Vector3 m_CellScale; |
|||
Vector3Int m_GridSize; |
|||
bool m_RotateWithAgent; |
|||
LayerMask m_ColliderMask; |
|||
GameObject m_RootReference; |
|||
string[] m_DetectableTags; |
|||
int m_InitialColliderBufferSize; |
|||
int m_MaxColliderBufferSize; |
|||
|
|||
int m_NumCells; |
|||
Vector3 m_HalfCellScale; |
|||
Vector3 m_CellCenterOffset; |
|||
Vector3[] m_CellLocalPositions; |
|||
Collider[] m_ColliderBuffer; |
|||
|
|||
public event Action<GameObject, int> GridOverlapDetectedAll; |
|||
public event Action<GameObject, int> GridOverlapDetectedClosest; |
|||
public event Action<GameObject, int> GridOverlapDetectedDebug; |
|||
|
|||
public BoxOverlapChecker( |
|||
Vector3 cellScale, |
|||
Vector3Int gridSize, |
|||
bool rotateWithAgent, |
|||
LayerMask colliderMask, |
|||
GameObject rootReference, |
|||
string[] detectableTags, |
|||
int initialColliderBufferSize, |
|||
int maxColliderBufferSize) |
|||
{ |
|||
m_CellScale = cellScale; |
|||
m_GridSize = gridSize; |
|||
m_RotateWithAgent = rotateWithAgent; |
|||
m_ColliderMask = colliderMask; |
|||
m_RootReference = rootReference; |
|||
m_DetectableTags = detectableTags; |
|||
m_InitialColliderBufferSize = initialColliderBufferSize; |
|||
m_MaxColliderBufferSize = maxColliderBufferSize; |
|||
|
|||
m_NumCells = gridSize.x * gridSize.z; |
|||
m_HalfCellScale = new Vector3(cellScale.x / 2f, cellScale.y, cellScale.z / 2f); |
|||
m_CellCenterOffset = new Vector3((gridSize.x - 1f) / 2, 0, (gridSize.z - 1f) / 2); |
|||
m_ColliderBuffer = new Collider[Math.Min(m_MaxColliderBufferSize, m_InitialColliderBufferSize)]; |
|||
|
|||
InitCellLocalPositions(); |
|||
} |
|||
|
|||
public bool RotateWithAgent |
|||
{ |
|||
get { return m_RotateWithAgent; } |
|||
set { m_RotateWithAgent = value; } |
|||
} |
|||
|
|||
public LayerMask ColliderMask |
|||
{ |
|||
get { return m_ColliderMask; } |
|||
set { m_ColliderMask = value; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes the local location of the cells
|
|||
/// </summary>
|
|||
void InitCellLocalPositions() |
|||
{ |
|||
m_CellLocalPositions = new Vector3[m_NumCells]; |
|||
|
|||
for (int i = 0; i < m_NumCells; i++) |
|||
{ |
|||
m_CellLocalPositions[i] = GetCellLocalPosition(i); |
|||
} |
|||
} |
|||
|
|||
/// <summary>Converts the index of the cell to the 3D point (y is zero) relative to grid center</summary>
|
|||
/// <returns>Vector3 of the position of the center of the cell relative to grid center</returns>
|
|||
/// <param name="cell">The index of the cell</param>
|
|||
Vector3 GetCellLocalPosition(int cellIndex) |
|||
{ |
|||
float x = (cellIndex / m_GridSize.z - m_CellCenterOffset.x) * m_CellScale.x; |
|||
float z = (cellIndex % m_GridSize.z - m_CellCenterOffset.z) * m_CellScale.z; |
|||
return new Vector3(x, 0, z); |
|||
} |
|||
|
|||
internal Vector3 GetCellGlobalPosition(int cellIndex) |
|||
{ |
|||
if (m_RotateWithAgent) |
|||
{ |
|||
return m_RootReference.transform.TransformPoint(m_CellLocalPositions[cellIndex]); |
|||
} |
|||
else |
|||
{ |
|||
return m_CellLocalPositions[cellIndex] + m_RootReference.transform.position; |
|||
} |
|||
} |
|||
|
|||
internal Quaternion GetGridRotation() |
|||
{ |
|||
return m_RotateWithAgent ? m_RootReference.transform.rotation : Quaternion.identity; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 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.
|
|||
/// </summary>
|
|||
/// <param name="cellCenter"></param>
|
|||
/// <param name="halfCellScale"></param>
|
|||
/// <param name="rotation"></param>
|
|||
/// <returns></returns>
|
|||
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, m_ColliderMask); |
|||
if (numFound == m_ColliderBuffer.Length && m_ColliderBuffer.Length < m_MaxColliderBufferSize) |
|||
{ |
|||
m_ColliderBuffer = new Collider[Math.Min(m_MaxColliderBufferSize, m_ColliderBuffer.Length * 2)]; |
|||
m_InitialColliderBufferSize = m_ColliderBuffer.Length; |
|||
} |
|||
else |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
return numFound; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Perceive the latest grid status. Call OverlapBoxNonAlloc once to detect colliders.
|
|||
/// Then parse the collider arrays according to all available gridSensor delegates.
|
|||
/// </summary>
|
|||
internal void Update() |
|||
{ |
|||
for (var cellIndex = 0; cellIndex < m_NumCells; cellIndex++) |
|||
{ |
|||
var cellCenter = GetCellGlobalPosition(cellIndex); |
|||
var numFound = BufferResizingOverlapBoxNonAlloc(cellCenter, m_HalfCellScale, GetGridRotation()); |
|||
|
|||
if (GridOverlapDetectedAll != null) |
|||
{ |
|||
ParseCollidersAll(m_ColliderBuffer, numFound, cellIndex, cellCenter, GridOverlapDetectedAll); |
|||
} |
|||
if (GridOverlapDetectedClosest != null) |
|||
{ |
|||
ParseCollidersClosest(m_ColliderBuffer, numFound, cellIndex, cellCenter, GridOverlapDetectedClosest); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Same as Update(), but only load data for debug gizmo.
|
|||
/// </summary>
|
|||
internal void UpdateGizmo() |
|||
{ |
|||
for (var cellIndex = 0; cellIndex < m_NumCells; cellIndex++) |
|||
{ |
|||
var cellCenter = GetCellGlobalPosition(cellIndex); |
|||
var numFound = BufferResizingOverlapBoxNonAlloc(cellCenter, m_HalfCellScale, GetGridRotation()); |
|||
|
|||
ParseCollidersClosest(m_ColliderBuffer, numFound, cellIndex, cellCenter, GridOverlapDetectedDebug); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Parses the array of colliders found within a cell. Finds the closest gameobject to the agent root reference within the cell
|
|||
/// </summary>
|
|||
void ParseCollidersClosest(Collider[] foundColliders, int numFound, int cellIndex, Vector3 cellCenter, Action<GameObject, int> detectedAction) |
|||
{ |
|||
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, m_RootReference)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var closestColliderPoint = foundColliders[i].ClosestPointOnBounds(cellCenter); |
|||
var currentDistanceSquared = (closestColliderPoint - m_RootReference.transform.position).sqrMagnitude; |
|||
|
|||
if (currentDistanceSquared >= minDistanceSquared) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// Checks if our colliders contain a detectable object
|
|||
var index = -1; |
|||
for (var ii = 0; ii < m_DetectableTags.Length; ii++) |
|||
{ |
|||
if (currentColliderGo.CompareTag(m_DetectableTags[ii])) |
|||
{ |
|||
index = ii; |
|||
break; |
|||
} |
|||
} |
|||
if (index > -1 && currentDistanceSquared < minDistanceSquared) |
|||
{ |
|||
minDistanceSquared = currentDistanceSquared; |
|||
closestColliderGo = currentColliderGo; |
|||
} |
|||
} |
|||
|
|||
if (!ReferenceEquals(closestColliderGo, null)) |
|||
{ |
|||
detectedAction.Invoke(closestColliderGo, cellIndex); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Parses all colliders in the array of colliders found within a cell.
|
|||
/// </summary>
|
|||
void ParseCollidersAll(Collider[] foundColliders, int numFound, int cellIndex, Vector3 cellCenter, Action<GameObject, int> detectedAction) |
|||
{ |
|||
for (int i = 0; i < numFound; i++) |
|||
{ |
|||
var currentColliderGo = foundColliders[i].gameObject; |
|||
if (!ReferenceEquals(currentColliderGo, m_RootReference)) |
|||
{ |
|||
detectedAction.Invoke(currentColliderGo, cellIndex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal void RegisterSensor(GridSensorBase sensor) |
|||
{ |
|||
if (sensor.GetProcessCollidersMethod() == ProcessCollidersMethod.ProcessAllColliders) |
|||
{ |
|||
GridOverlapDetectedAll += sensor.ProcessDetectedObject; |
|||
} |
|||
else |
|||
{ |
|||
GridOverlapDetectedClosest += sensor.ProcessDetectedObject; |
|||
} |
|||
} |
|||
|
|||
internal void RegisterDebugSensor(GridSensorBase debugSensor) |
|||
{ |
|||
GridOverlapDetectedDebug += debugSensor.ProcessDetectedObject; |
|||
} |
|||
} |
|||
} |
|
|||
using UnityEngine; |
|||
using Unity.MLAgents.Sensors; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Sensors |
|||
{ |
|||
/// <summary>
|
|||
/// Grid-based sensor that counts the number of detctable objects.
|
|||
/// </summary>
|
|||
public class CountingGridSensor : GridSensorBase |
|||
{ |
|||
/// <summary>
|
|||
/// Create a CountingGridSensor with the specified configuration.
|
|||
/// </summary>
|
|||
/// <param name="name">The sensor name</param>
|
|||
/// <param name="cellScale">The scale of each cell in the grid</param>
|
|||
/// <param name="gridNum">Number of cells on each side of the grid</param>
|
|||
/// <param name="detectableTags">Tags to be detected by the sensor</param>
|
|||
/// <param name="compression">Compression type</param>
|
|||
public CountingGridSensor( |
|||
string name, |
|||
Vector3 cellScale, |
|||
Vector3Int gridNum, |
|||
string[] detectableTags, |
|||
SensorCompressionType compression |
|||
) : base(name, cellScale, gridNum, detectableTags, compression) |
|||
{ |
|||
CompressionType = SensorCompressionType.None; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override int GetCellObservationSize() |
|||
{ |
|||
return DetectableTags == null ? 0 : DetectableTags.Length; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override bool IsDataNormalized() |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected internal override ProcessCollidersMethod GetProcessCollidersMethod() |
|||
{ |
|||
return ProcessCollidersMethod.ProcessAllColliders; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get object counts for each detectable tags detected in a cell.
|
|||
/// </summary>
|
|||
/// <param name="detectedObject">The game object that was detected within a certain cell</param>
|
|||
/// <param name="tagIndex">The index of the detectedObject's tag in the DetectableObjects list</param>
|
|||
/// <param name="dataBuffer">The buffer to write the observation values.
|
|||
/// The buffer size is configured by <seealso cref="GetCellObservationSize"/>.
|
|||
/// </param>
|
|||
protected override void GetObjectData(GameObject detectedObject, int tagIndex, float[] dataBuffer) |
|||
{ |
|||
dataBuffer[tagIndex] += 1; |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.CompilerServices; |
|||
using UnityEngine; |
|||
using Unity.MLAgents.Sensors; |
|||
using UnityEngine.Profiling; |
|||
using Object = UnityEngine.Object; |
|||
|
|||
[assembly: InternalsVisibleTo("Unity.ML-Agents.Extensions.EditorTests")] |
|||
namespace Unity.MLAgents.Extensions.Sensors |
|||
{ |
|||
/// <summary>
|
|||
/// The way the GridSensor process detected colliders in a cell.
|
|||
/// </summary>
|
|||
public enum ProcessCollidersMethod |
|||
{ |
|||
/// <summary>
|
|||
/// Get data from all colliders detected in a cell
|
|||
/// </summary>
|
|||
ProcessAllColliders, |
|||
|
|||
/// <summary>
|
|||
/// Get data from the collider closest to the agent
|
|||
/// </summary>
|
|||
ProcessClosestColliders |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Grid-based sensor.
|
|||
/// </summary>
|
|||
public class GridSensorBase : ISensor, IBuiltInSensor, IDisposable |
|||
{ |
|||
string m_Name; |
|||
Vector3 m_CellScale; |
|||
Vector3Int m_GridSize; |
|||
string[] m_DetectableTags; |
|||
SensorCompressionType m_CompressionType; |
|||
ObservationSpec m_ObservationSpec; |
|||
internal BoxOverlapChecker m_BoxOverlapChecker; |
|||
|
|||
// Buffers
|
|||
float[] m_PerceptionBuffer; |
|||
Color[] m_PerceptionColors; |
|||
Texture2D m_PerceptionTexture; |
|||
float[] m_CellDataBuffer; |
|||
|
|||
// Utility Constants Calculated on Init
|
|||
int m_NumCells; |
|||
int m_CellObservationSize; |
|||
Vector3 m_CellCenterOffset; |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Create a GridSensorBase with the specified configuration.
|
|||
/// </summary>
|
|||
/// <param name="name">The sensor name</param>
|
|||
/// <param name="cellScale">The scale of each cell in the grid</param>
|
|||
/// <param name="gridNum">Number of cells on each side of the grid</param>
|
|||
/// <param name="detectableTags">Tags to be detected by the sensor</param>
|
|||
/// <param name="compression">Compression type</param>
|
|||
public GridSensorBase( |
|||
string name, |
|||
Vector3 cellScale, |
|||
Vector3Int gridNum, |
|||
string[] detectableTags, |
|||
SensorCompressionType compression |
|||
) |
|||
{ |
|||
m_Name = name; |
|||
m_CellScale = cellScale; |
|||
m_GridSize = gridNum; |
|||
m_DetectableTags = detectableTags; |
|||
CompressionType = compression; |
|||
|
|||
if (m_GridSize.y != 1) |
|||
{ |
|||
throw new UnityAgentsException("GridSensor only supports 2D grids."); |
|||
} |
|||
|
|||
m_NumCells = m_GridSize.x * m_GridSize.z; |
|||
m_CellObservationSize = GetCellObservationSize(); |
|||
m_ObservationSpec = ObservationSpec.Visual(m_GridSize.x, m_GridSize.z, m_CellObservationSize); |
|||
m_PerceptionTexture = new Texture2D(m_GridSize.x, m_GridSize.z, TextureFormat.RGB24, false); |
|||
|
|||
ResetPerceptionBuffer(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The compression type used by the sensor.
|
|||
/// </summary>
|
|||
public SensorCompressionType CompressionType |
|||
{ |
|||
get { return m_CompressionType; } |
|||
set |
|||
{ |
|||
if (!IsDataNormalized() && value == SensorCompressionType.PNG) |
|||
{ |
|||
Debug.LogWarning($"Compression type {value} is only supported with normalized data. " + |
|||
"The sensor will not compress the data."); |
|||
return; |
|||
} |
|||
m_CompressionType = value; |
|||
} |
|||
} |
|||
|
|||
internal float[] PerceptionBuffer |
|||
{ |
|||
get { return m_PerceptionBuffer; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The tags which the sensor dectects.
|
|||
/// </summary>
|
|||
protected string[] DetectableTags |
|||
{ |
|||
get { return m_DetectableTags; } |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Reset() { } |
|||
|
|||
/// <summary>
|
|||
/// Clears the perception buffer before loading in new data.
|
|||
/// </summary>
|
|||
public void ResetPerceptionBuffer() |
|||
{ |
|||
if (m_PerceptionBuffer != null) |
|||
{ |
|||
Array.Clear(m_PerceptionBuffer, 0, m_PerceptionBuffer.Length); |
|||
Array.Clear(m_CellDataBuffer, 0, m_CellDataBuffer.Length); |
|||
} |
|||
else |
|||
{ |
|||
m_PerceptionBuffer = new float[m_CellObservationSize * m_NumCells]; |
|||
m_CellDataBuffer = new float[m_CellObservationSize]; |
|||
m_PerceptionColors = new Color[m_NumCells]; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public string GetName() |
|||
{ |
|||
return m_Name; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public CompressionSpec GetCompressionSpec() |
|||
{ |
|||
return new CompressionSpec(CompressionType); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public BuiltInSensorType GetBuiltInSensorType() |
|||
{ |
|||
return BuiltInSensorType.GridSensor; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public byte[] GetCompressedObservation() |
|||
{ |
|||
using (TimerStack.Instance.Scoped("GridSensor.GetCompressedObservation")) |
|||
{ |
|||
var allBytes = new List<byte>(); |
|||
var numImages = (m_CellObservationSize + 2) / 3; |
|||
for (int i = 0; i < numImages; i++) |
|||
{ |
|||
var channelIndex = 3 * i; |
|||
GridValuesToTexture(channelIndex, Math.Min(3, m_CellObservationSize - channelIndex)); |
|||
allBytes.AddRange(m_PerceptionTexture.EncodeToPNG()); |
|||
} |
|||
|
|||
return allBytes.ToArray(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Convert observation values to texture for PNG compression.
|
|||
/// </summary>
|
|||
void GridValuesToTexture(int channelIndex, int numChannelsToAdd) |
|||
{ |
|||
for (int i = 0; i < m_NumCells; i++) |
|||
{ |
|||
for (int j = 0; j < numChannelsToAdd; j++) |
|||
{ |
|||
m_PerceptionColors[i][j] = m_PerceptionBuffer[i * m_CellObservationSize + channelIndex + j]; |
|||
} |
|||
} |
|||
m_PerceptionTexture.SetPixels(m_PerceptionColors); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the observation values of the detected game object.
|
|||
/// Default is to record the detected tag index.
|
|||
///
|
|||
/// This method can be overridden to encode the observation differently or get custom data from the object.
|
|||
/// When overriding this method, <seealso cref="GetCellObservationSize"/> and <seealso cref="IsDataNormalized"/>
|
|||
/// might also need to change accordingly.
|
|||
/// </summary>
|
|||
/// <param name="detectedObject">The game object that was detected within a certain cell</param>
|
|||
/// <param name="tagIndex">The index of the detectedObject's tag in the DetectableObjects list</param>
|
|||
/// <param name="dataBuffer">The buffer to write the observation values.
|
|||
/// The buffer size is configured by <seealso cref="GetCellObservationSize"/>.
|
|||
/// </param>
|
|||
/// <example>
|
|||
/// Here is an example of overriding GetObjectData to get the velocity of a potential Rigidbody:
|
|||
/// <code>
|
|||
/// protected override void GetObjectData(GameObject detectedObject, int tagIndex, float[] dataBuffer)
|
|||
/// {
|
|||
/// if (tagIndex == Array.IndexOf(DetectableTags, "RigidBodyObject"))
|
|||
/// {
|
|||
/// Rigidbody rigidbody = detectedObject.GetComponent<Rigidbody>();
|
|||
/// dataBuffer[0] = rigidbody.velocity.x;
|
|||
/// dataBuffer[1] = rigidbody.velocity.y;
|
|||
/// dataBuffer[2] = rigidbody.velocity.z;
|
|||
/// }
|
|||
/// }
|
|||
/// </code>
|
|||
/// </example>
|
|||
protected virtual void GetObjectData(GameObject detectedObject, int tagIndex, float[] dataBuffer) |
|||
{ |
|||
dataBuffer[0] = tagIndex + 1; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the observation size for each cell. This will be the size of dataBuffer for <seealso cref="GetObjectData"/>.
|
|||
/// If overriding <seealso cref="GetObjectData"/>, override this method as well to the custom observation size.
|
|||
/// </summary>
|
|||
/// <returns>The observation size of each cell.</returns>
|
|||
protected virtual int GetCellObservationSize() |
|||
{ |
|||
return 1; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Whether the data is normalized within [0, 1]. The sensor can only use PNG compression if the data is normailzed.
|
|||
/// If overriding <seealso cref="GetObjectData"/>, override this method as well according to the custom observation values.
|
|||
/// </summary>
|
|||
/// <returns>Bool value indicating whether data is normalized.</returns>
|
|||
protected virtual bool IsDataNormalized() |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Whether to process all detected colliders in a cell. Default to false and only use the one closest to the agent.
|
|||
/// If overriding <seealso cref="GetObjectData"/>, consider override this method when needed.
|
|||
/// </summary>
|
|||
/// <returns>Bool value indicating whether to process all detected colliders in a cell.</returns>
|
|||
protected internal virtual ProcessCollidersMethod GetProcessCollidersMethod() |
|||
{ |
|||
return ProcessCollidersMethod.ProcessClosestColliders; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// If using PNG compression, check if the values are normalized.
|
|||
/// </summary>
|
|||
void ValidateValues(float[] dataValues, GameObject detectedObject) |
|||
{ |
|||
if (m_CompressionType != SensorCompressionType.PNG) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
for (int j = 0; j < dataValues.Length; j++) |
|||
{ |
|||
if (dataValues[j] < 0 || dataValues[j] > 1) |
|||
throw new UnityAgentsException($"When using compression type {m_CompressionType} the data value has to be normalized between 0-1. " + |
|||
$"Received value[{dataValues[j]}] for {detectedObject.name}"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Collect data from the detected object if a detectable tag is matched.
|
|||
/// </summary>
|
|||
internal void ProcessDetectedObject(GameObject detectedObject, int cellIndex) |
|||
{ |
|||
Profiler.BeginSample("GridSensor.ProcessDetectedObject"); |
|||
for (var i = 0; i < m_DetectableTags.Length; i++) |
|||
{ |
|||
if (!ReferenceEquals(detectedObject, null) && detectedObject.CompareTag(m_DetectableTags[i])) |
|||
{ |
|||
if (GetProcessCollidersMethod() == ProcessCollidersMethod.ProcessAllColliders) |
|||
{ |
|||
Array.Copy(m_PerceptionBuffer, cellIndex * m_CellObservationSize, m_CellDataBuffer, 0, m_CellObservationSize); |
|||
} |
|||
else |
|||
{ |
|||
Array.Clear(m_CellDataBuffer, 0, m_CellDataBuffer.Length); |
|||
} |
|||
|
|||
GetObjectData(detectedObject, i, m_CellDataBuffer); |
|||
ValidateValues(m_CellDataBuffer, detectedObject); |
|||
Array.Copy(m_CellDataBuffer, 0, m_PerceptionBuffer, cellIndex * m_CellObservationSize, m_CellObservationSize); |
|||
break; |
|||
} |
|||
} |
|||
Profiler.EndSample(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Update() |
|||
{ |
|||
ResetPerceptionBuffer(); |
|||
using (TimerStack.Instance.Scoped("GridSensor.Update")) |
|||
{ |
|||
if (m_BoxOverlapChecker != null) |
|||
{ |
|||
m_BoxOverlapChecker.Update(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public ObservationSpec GetObservationSpec() |
|||
{ |
|||
return m_ObservationSpec; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public int Write(ObservationWriter writer) |
|||
{ |
|||
using (TimerStack.Instance.Scoped("GridSensor.Write")) |
|||
{ |
|||
int index = 0; |
|||
for (var h = m_GridSize.z - 1; h >= 0; h--) |
|||
{ |
|||
for (var w = 0; w < m_GridSize.x; w++) |
|||
{ |
|||
for (var d = 0; d < m_CellObservationSize; d++) |
|||
{ |
|||
writer[h, w, d] = m_PerceptionBuffer[index]; |
|||
index++; |
|||
} |
|||
} |
|||
} |
|||
return index; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clean up the internal objects.
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
if (!ReferenceEquals(null, m_PerceptionTexture)) |
|||
{ |
|||
DestroyTexture(m_PerceptionTexture); |
|||
m_PerceptionTexture = null; |
|||
} |
|||
} |
|||
|
|||
static void DestroyTexture(Texture2D texture) |
|||
{ |
|||
if (Application.isEditor) |
|||
{ |
|||
// Edit Mode tests complain if we use Destroy()
|
|||
// TODO move to extension methods for UnityEngine.Object?
|
|||
Object.DestroyImmediate(texture); |
|||
} |
|||
else |
|||
{ |
|||
Object.Destroy(texture); |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
using UnityEngine; |
|||
using Unity.MLAgents.Sensors; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Sensors |
|||
{ |
|||
/// <summary>
|
|||
/// Grid-based sensor with one-hot observations.
|
|||
/// </summary>
|
|||
public class OneHotGridSensor : GridSensorBase |
|||
{ |
|||
/// <summary>
|
|||
/// Create a OneHotGridSensor with the specified configuration.
|
|||
/// </summary>
|
|||
/// <param name="name">The sensor name</param>
|
|||
/// <param name="cellScale">The scale of each cell in the grid</param>
|
|||
/// <param name="gridNum">Number of cells on each side of the grid</param>
|
|||
/// <param name="detectableTags">Tags to be detected by the sensor</param>
|
|||
/// <param name="compression">Compression type</param>
|
|||
public OneHotGridSensor( |
|||
string name, |
|||
Vector3 cellScale, |
|||
Vector3Int gridNum, |
|||
string[] detectableTags, |
|||
SensorCompressionType compression |
|||
) : base(name, cellScale, gridNum, detectableTags, compression) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override int GetCellObservationSize() |
|||
{ |
|||
return DetectableTags == null ? 0 : DetectableTags.Length; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override bool IsDataNormalized() |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected internal override ProcessCollidersMethod GetProcessCollidersMethod() |
|||
{ |
|||
return ProcessCollidersMethod.ProcessClosestColliders; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the one-hot representation of the detected game object's tag.
|
|||
/// </summary>
|
|||
/// <param name="detectedObject">The game object that was detected within a certain cell</param>
|
|||
/// <param name="tagIndex">The index of the detectedObject's tag in the DetectableObjects list</param>
|
|||
/// <param name="dataBuffer">The buffer to write the observation values.
|
|||
/// The buffer size is configured by <seealso cref="GetCellObservationSize"/>.
|
|||
/// </param>
|
|||
protected override void GetObjectData(GameObject detectedObject, int tagIndex, float[] dataBuffer) |
|||
{ |
|||
dataBuffer[tagIndex] = 1; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 4690c621901ab49f2a557fa255c46622 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System.Collections.Generic; |
|||
using System.Reflection; |
|||
using NUnit.Framework; |
|||
using UnityEngine; |
|||
using Unity.MLAgents.Extensions.Sensors; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Tests.GridSensors |
|||
{ |
|||
internal class TestBoxOverlapChecker : BoxOverlapChecker |
|||
{ |
|||
public TestBoxOverlapChecker( |
|||
Vector3 cellScale, |
|||
Vector3Int gridSize, |
|||
bool rotateWithAgent, |
|||
LayerMask colliderMask, |
|||
GameObject rootReference, |
|||
string[] detectableTags, |
|||
int initialColliderBufferSize, |
|||
int maxColliderBufferSize |
|||
) : base( |
|||
cellScale, |
|||
gridSize, |
|||
rotateWithAgent, |
|||
colliderMask, |
|||
rootReference, |
|||
detectableTags, |
|||
initialColliderBufferSize, |
|||
maxColliderBufferSize) |
|||
{ } |
|||
|
|||
public Vector3[] CellLocalPositions |
|||
{ |
|||
get |
|||
{ |
|||
return (Vector3[])typeof(BoxOverlapChecker).GetField("m_CellLocalPositions", |
|||
BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this); |
|||
} |
|||
} |
|||
|
|||
public Collider[] ColliderBuffer |
|||
{ |
|||
get |
|||
{ |
|||
return (Collider[])typeof(BoxOverlapChecker).GetField("m_ColliderBuffer", |
|||
BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this); |
|||
} |
|||
} |
|||
|
|||
public static TestBoxOverlapChecker CreateChecker( |
|||
float cellScaleX = 1f, |
|||
float cellScaleZ = 1f, |
|||
int gridSizeX = 10, |
|||
int gridSizeZ = 10, |
|||
bool rotateWithAgent = true, |
|||
GameObject rootReference = null, |
|||
string[] detectableTags = null, |
|||
int initialColliderBufferSize = 4, |
|||
int maxColliderBufferSize = 500) |
|||
{ |
|||
return new TestBoxOverlapChecker( |
|||
new Vector3(cellScaleX, 0.01f, cellScaleZ), |
|||
new Vector3Int(gridSizeX, 1, gridSizeZ), |
|||
rotateWithAgent, |
|||
LayerMask.GetMask("Default"), |
|||
rootReference, |
|||
detectableTags, |
|||
initialColliderBufferSize, |
|||
maxColliderBufferSize); |
|||
} |
|||
} |
|||
|
|||
public class BoxOverlapCheckerTests |
|||
{ |
|||
[Test] |
|||
public void TestCellLocalPosition() |
|||
{ |
|||
var testGo = new GameObject("test"); |
|||
testGo.transform.position = Vector3.zero; |
|||
var boxOverlapSquare = TestBoxOverlapChecker.CreateChecker(gridSizeX: 10, gridSizeZ: 10, rotateWithAgent: false, rootReference: testGo); |
|||
|
|||
var localPos = boxOverlapSquare.CellLocalPositions; |
|||
Assert.AreEqual(new Vector3(-4.5f, 0, -4.5f), localPos[0]); |
|||
Assert.AreEqual(new Vector3(-4.5f, 0, 4.5f), localPos[9]); |
|||
Assert.AreEqual(new Vector3(4.5f, 0, -4.5f), localPos[90]); |
|||
Assert.AreEqual(new Vector3(4.5f, 0, 4.5f), localPos[99]); |
|||
Object.DestroyImmediate(testGo); |
|||
|
|||
var testGo2 = new GameObject("test"); |
|||
testGo2.transform.position = new Vector3(3.5f, 8f, 17f); // random, should have no effect on local positions
|
|||
var boxOverlapRect = TestBoxOverlapChecker.CreateChecker(gridSizeX: 5, gridSizeZ: 15, rotateWithAgent: true, rootReference: testGo); |
|||
|
|||
localPos = boxOverlapRect.CellLocalPositions; |
|||
Assert.AreEqual(new Vector3(-2f, 0, -7f), localPos[0]); |
|||
Assert.AreEqual(new Vector3(-2f, 0, 7f), localPos[14]); |
|||
Assert.AreEqual(new Vector3(2f, 0, -7f), localPos[60]); |
|||
Assert.AreEqual(new Vector3(2f, 0, 7f), localPos[74]); |
|||
Object.DestroyImmediate(testGo2); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestCellGlobalPositionNoRotate() |
|||
{ |
|||
var testGo = new GameObject("test"); |
|||
var position = new Vector3(3.5f, 8f, 17f); |
|||
testGo.transform.position = position; |
|||
var boxOverlap = TestBoxOverlapChecker.CreateChecker(gridSizeX: 10, gridSizeZ: 10, rotateWithAgent: false, rootReference: testGo); |
|||
|
|||
Assert.AreEqual(new Vector3(-4.5f, 0, -4.5f) + position, boxOverlap.GetCellGlobalPosition(0)); |
|||
Assert.AreEqual(new Vector3(-4.5f, 0, 4.5f) + position, boxOverlap.GetCellGlobalPosition(9)); |
|||
Assert.AreEqual(new Vector3(4.5f, 0, -4.5f) + position, boxOverlap.GetCellGlobalPosition(90)); |
|||
Assert.AreEqual(new Vector3(4.5f, 0, 4.5f) + position, boxOverlap.GetCellGlobalPosition(99)); |
|||
|
|||
testGo.transform.Rotate(0, 90, 0); // should have no effect on positions
|
|||
Assert.AreEqual(new Vector3(-4.5f, 0, -4.5f) + position, boxOverlap.GetCellGlobalPosition(0)); |
|||
Assert.AreEqual(new Vector3(-4.5f, 0, 4.5f) + position, boxOverlap.GetCellGlobalPosition(9)); |
|||
Assert.AreEqual(new Vector3(4.5f, 0, -4.5f) + position, boxOverlap.GetCellGlobalPosition(90)); |
|||
Assert.AreEqual(new Vector3(4.5f, 0, 4.5f) + position, boxOverlap.GetCellGlobalPosition(99)); |
|||
|
|||
Object.DestroyImmediate(testGo); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestCellGlobalPositionRotate() |
|||
{ |
|||
var testGo = new GameObject("test"); |
|||
var position = new Vector3(15f, 6f, 13f); |
|||
testGo.transform.position = position; |
|||
var boxOverlap = TestBoxOverlapChecker.CreateChecker(gridSizeX: 5, gridSizeZ: 15, rotateWithAgent: true, rootReference: testGo); |
|||
|
|||
Assert.AreEqual(new Vector3(-2f, 0, -7f) + position, boxOverlap.GetCellGlobalPosition(0)); |
|||
Assert.AreEqual(new Vector3(-2f, 0, 7f) + position, boxOverlap.GetCellGlobalPosition(14)); |
|||
Assert.AreEqual(new Vector3(2f, 0, -7f) + position, boxOverlap.GetCellGlobalPosition(60)); |
|||
Assert.AreEqual(new Vector3(2f, 0, 7f) + position, boxOverlap.GetCellGlobalPosition(74)); |
|||
|
|||
testGo.transform.Rotate(0, 90, 0); |
|||
// round to int to ignore numeric errors
|
|||
Assert.AreEqual(Vector3Int.RoundToInt(new Vector3(-7f, 0, 2f) + position), Vector3Int.RoundToInt(boxOverlap.GetCellGlobalPosition(0))); |
|||
Assert.AreEqual(Vector3Int.RoundToInt(new Vector3(7f, 0, 2f) + position), Vector3Int.RoundToInt(boxOverlap.GetCellGlobalPosition(14))); |
|||
Assert.AreEqual(Vector3Int.RoundToInt(new Vector3(-7f, 0, -2f) + position), Vector3Int.RoundToInt(boxOverlap.GetCellGlobalPosition(60))); |
|||
Assert.AreEqual(Vector3Int.RoundToInt(new Vector3(7f, 0, -2f) + position), Vector3Int.RoundToInt(boxOverlap.GetCellGlobalPosition(74))); |
|||
|
|||
Object.DestroyImmediate(testGo); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestBufferResize() |
|||
{ |
|||
List<GameObject> testObjects = new List<GameObject>(); |
|||
var testGo = new GameObject("test"); |
|||
testGo.transform.position = Vector3.zero; |
|||
testObjects.Add(testGo); |
|||
var boxOverlap = TestBoxOverlapChecker.CreateChecker(rootReference: testGo, initialColliderBufferSize: 2, maxColliderBufferSize: 5); |
|||
boxOverlap.Update(); |
|||
Assert.AreEqual(2, boxOverlap.ColliderBuffer.Length); |
|||
|
|||
for (var i = 0; i < 3; i++) |
|||
{ |
|||
var boxGo = new GameObject("test"); |
|||
boxGo.transform.position = Vector3.zero; |
|||
boxGo.AddComponent<BoxCollider>(); |
|||
testObjects.Add(boxGo); |
|||
} |
|||
boxOverlap.Update(); |
|||
Assert.AreEqual(4, boxOverlap.ColliderBuffer.Length); |
|||
|
|||
for (var i = 0; i < 2; i++) |
|||
{ |
|||
var boxGo = new GameObject("test"); |
|||
boxGo.transform.position = Vector3.zero; |
|||
boxGo.AddComponent<BoxCollider>(); |
|||
testObjects.Add(boxGo); |
|||
} |
|||
boxOverlap.Update(); |
|||
Assert.AreEqual(5, boxOverlap.ColliderBuffer.Length); |
|||
|
|||
Object.DestroyImmediate(testGo); |
|||
foreach (var go in testObjects) |
|||
{ |
|||
Object.DestroyImmediate(go); |
|||
} |
|||
} |
|||
|
|||
[Test] |
|||
public void TestParseCollidersClosest() |
|||
{ |
|||
var tag1 = "Player"; |
|||
List<GameObject> testObjects = new List<GameObject>(); |
|||
var testGo = new GameObject("test"); |
|||
testGo.transform.position = Vector3.zero; |
|||
var boxOverlap = TestBoxOverlapChecker.CreateChecker( |
|||
cellScaleX: 10f, |
|||
cellScaleZ: 10f, |
|||
gridSizeX: 2, |
|||
gridSizeZ: 2, |
|||
rootReference: testGo, |
|||
detectableTags: new string[] { tag1 }); |
|||
var helper = new VerifyParseCollidersHelper(); |
|||
boxOverlap.GridOverlapDetectedClosest += helper.DetectedAction; |
|||
|
|||
for (var i = 0; i < 3; i++) |
|||
{ |
|||
var boxGo = new GameObject("test"); |
|||
boxGo.transform.position = new Vector3(i + 1, 0, 1); |
|||
boxGo.AddComponent<BoxCollider>(); |
|||
boxGo.tag = tag1; |
|||
testObjects.Add(boxGo); |
|||
} |
|||
|
|||
boxOverlap.Update(); |
|||
helper.Verify(1, new List<GameObject> { testObjects[0] }); |
|||
|
|||
Object.DestroyImmediate(testGo); |
|||
foreach (var go in testObjects) |
|||
{ |
|||
Object.DestroyImmediate(go); |
|||
} |
|||
} |
|||
|
|||
[Test] |
|||
public void TestParseCollidersAll() |
|||
{ |
|||
var tag1 = "Player"; |
|||
List<GameObject> testObjects = new List<GameObject>(); |
|||
var testGo = new GameObject("test"); |
|||
testGo.transform.position = Vector3.zero; |
|||
var boxOverlap = TestBoxOverlapChecker.CreateChecker( |
|||
cellScaleX: 10f, |
|||
cellScaleZ: 10f, |
|||
gridSizeX: 2, |
|||
gridSizeZ: 2, |
|||
rootReference: testGo, |
|||
detectableTags: new string[] { tag1 }); |
|||
var helper = new VerifyParseCollidersHelper(); |
|||
boxOverlap.GridOverlapDetectedAll += helper.DetectedAction; |
|||
|
|||
for (var i = 0; i < 3; i++) |
|||
{ |
|||
var boxGo = new GameObject("test"); |
|||
boxGo.transform.position = new Vector3(i + 1, 0, 1); |
|||
boxGo.AddComponent<BoxCollider>(); |
|||
boxGo.tag = tag1; |
|||
testObjects.Add(boxGo); |
|||
} |
|||
|
|||
boxOverlap.Update(); |
|||
helper.Verify(3, testObjects); |
|||
|
|||
Object.DestroyImmediate(testGo); |
|||
foreach (var go in testObjects) |
|||
{ |
|||
Object.DestroyImmediate(go); |
|||
} |
|||
} |
|||
|
|||
public class VerifyParseCollidersHelper |
|||
{ |
|||
int m_NumInvoked; |
|||
List<GameObject> m_ParsedObjects = new List<GameObject>(); |
|||
|
|||
public void DetectedAction(GameObject go, int cellIndex) |
|||
{ |
|||
m_NumInvoked += 1; |
|||
m_ParsedObjects.Add(go); |
|||
} |
|||
|
|||
public void Verify(int expectNumInvoke, List<GameObject> expectedObjects) |
|||
{ |
|||
Assert.AreEqual(expectNumInvoke, m_NumInvoked); |
|||
Assert.AreEqual(expectedObjects.Count, m_ParsedObjects.Count); |
|||
foreach (var obj in expectedObjects) |
|||
{ |
|||
Assert.Contains(obj, m_ParsedObjects); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Test] |
|||
public void TestOnlyOneChecker() |
|||
{ |
|||
var testGo = new GameObject("test"); |
|||
testGo.transform.position = Vector3.zero; |
|||
var gridSensorComponent = testGo.AddComponent<SimpleTestGridSensorComponent>(); |
|||
gridSensorComponent.SetComponentParameters(useGridSensorBase: true, useOneHotTag: true, countColliders: true); |
|||
var sensors = gridSensorComponent.CreateSensors(); |
|||
int numChecker = 0; |
|||
foreach (var sensor in sensors) |
|||
{ |
|||
var gridsensor = (GridSensorBase)sensor; |
|||
if (gridsensor.m_BoxOverlapChecker != null) |
|||
{ |
|||
numChecker += 1; |
|||
} |
|||
} |
|||
Assert.AreEqual(1, numChecker); |
|||
} |
|||
} |
|||
} |
|
|||
using NUnit.Framework; |
|||
using System; |
|||
using System.Linq; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Tests.GridSensors |
|||
{ |
|||
public static class GridObsTestUtils |
|||
{ |
|||
/// <summary>
|
|||
/// Utility function to duplicate an array into an array of arrays
|
|||
/// </summary>
|
|||
/// <param name="array">array to duplicate</param>
|
|||
/// <param name="numCopies">number of times to duplicate</param>
|
|||
/// <returns>array of duplicated arrays</returns>
|
|||
public static float[][] DuplicateArray(float[] array, int numCopies) |
|||
{ |
|||
float[][] duplicated = new float[numCopies][]; |
|||
for (int i = 0; i < numCopies; i++) |
|||
{ |
|||
duplicated[i] = array; |
|||
} |
|||
return duplicated; |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Asserts that the sub-arrays of the total array are equal to specific subarrays at specific subarray indicies and equal to a default everywhere else.
|
|||
/// </summary>
|
|||
/// <param name="total">Array containing all data of the grid observation. Is a concatenation of N subarrays all of the same length</param>
|
|||
/// <param name="indicies">The indicies to verify that differ from the default array</param>
|
|||
/// <param name="expectedArrays">The sub arrays values that differ from the default array</param>
|
|||
/// <param name="expectedDefaultArray">The default value of a sub array</param>
|
|||
/// <example>
|
|||
/// If the total array is data from a 4x4x2 grid observation, total will be an array of size 32 and each sub array will have a size of 2.
|
|||
/// Let 3 cells at indicies (0, 1), (2, 2), and (3, 0) with values ([.1, .5]), ([.9, .7]), ([0, .2]), respectively.
|
|||
/// If the default values of cells are ([0, 0]) then the grid observation will be as follows:
|
|||
/// [ [0, 0], [.1, .5], [ 0, 0 ], [0, 0],
|
|||
/// [0, 0], [ 0, 0 ], [ 0, 0 ], [0, 0],
|
|||
/// [0, 0], [ 0, 0 ], [.9, .7], [0, 0],
|
|||
/// [0, .2], [ 0, 0 ], [ 0, 0 ], [0, 0] ]
|
|||
///
|
|||
/// Which will make the total array will be the flattened array
|
|||
/// total = [0, 0, .1, .5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, .9, .7, 0, 0, 0, .2, 0, 0, 0, 0, 0]
|
|||
///
|
|||
/// The indicies of the activated cells in the flattened array will be 1, 10, and 12
|
|||
///
|
|||
/// So to verify that the total array is as expected, AssertSubarraysAtIndex should be called as
|
|||
/// AssertSubarraysAtIndex(
|
|||
/// total,
|
|||
/// indicies = new int[] {1, 10, 12},
|
|||
/// expectedArrays = new float[][] { new float[] {.1, .5}, new float[] {.9, .7}, new float[] {0, .2}},
|
|||
/// expecedDefaultArray = new float[] {0, 0}
|
|||
/// )
|
|||
/// </example>
|
|||
public static void AssertSubarraysAtIndex(float[] total, int[] indicies, float[][] expectedArrays, float[] expectedDefaultArray) |
|||
{ |
|||
int totalIndex = 0; |
|||
int subIndex = 0; |
|||
int subarrayIndex = 0; |
|||
int lenOfData = expectedDefaultArray.Length; |
|||
int numArrays = total.Length / lenOfData; |
|||
for (int i = 0; i < numArrays; i++) |
|||
{ |
|||
totalIndex = i * lenOfData; |
|||
|
|||
if (indicies.Contains(i)) |
|||
{ |
|||
subarrayIndex = Array.IndexOf(indicies, i); |
|||
for (subIndex = 0; subIndex < lenOfData; subIndex++) |
|||
{ |
|||
Assert.AreEqual(expectedArrays[subarrayIndex][subIndex], total[totalIndex], |
|||
"Expected " + expectedArrays[subarrayIndex][subIndex] + " at subarray index " + totalIndex + ", index = " + subIndex + " but was " + total[totalIndex]); |
|||
totalIndex++; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
for (subIndex = 0; subIndex < lenOfData; subIndex++) |
|||
{ |
|||
Assert.AreEqual(expectedDefaultArray[subIndex], total[totalIndex], |
|||
"Expected default value " + expectedDefaultArray[subIndex] + " at subarray index " + totalIndex + ", index = " + subIndex + " but was " + total[totalIndex]); |
|||
totalIndex++; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
using System.Collections; |
|||
using NUnit.Framework; |
|||
using UnityEngine; |
|||
using UnityEngine.TestTools; |
|||
using Unity.MLAgents.Sensors; |
|||
using Unity.MLAgents.Extensions.Sensors; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Tests.GridSensors |
|||
{ |
|||
public class GridSensorTests |
|||
{ |
|||
GameObject testGo; |
|||
GameObject boxGo; |
|||
SimpleTestGridSensorComponent gridSensorComponent; |
|||
|
|||
// Use built-in tags
|
|||
const string k_Tag1 = "Player"; |
|||
const string k_Tag2 = "Respawn"; |
|||
|
|||
[UnitySetUp] |
|||
public IEnumerator SetupScene() |
|||
{ |
|||
testGo = new GameObject("test"); |
|||
testGo.transform.position = Vector3.zero; |
|||
gridSensorComponent = testGo.AddComponent<SimpleTestGridSensorComponent>(); |
|||
|
|||
boxGo = new GameObject("block"); |
|||
boxGo.tag = k_Tag1; |
|||
boxGo.transform.position = new Vector3(3f, 0f, 3f); |
|||
boxGo.AddComponent<BoxCollider>(); |
|||
|
|||
TestGridSensorConfig.Reset(); |
|||
yield return null; |
|||
} |
|||
|
|||
[TearDown] |
|||
public void ClearScene() |
|||
{ |
|||
Object.DestroyImmediate(boxGo); |
|||
Object.DestroyImmediate(testGo); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestBufferSize() |
|||
{ |
|||
testGo.tag = k_Tag2; |
|||
string[] tags = { k_Tag1, k_Tag2 }; |
|||
gridSensorComponent.SetComponentParameters(tags, gridSizeX: 3, gridSizeZ: 4, useTestingGridSensor: true); |
|||
TestGridSensorConfig.SetParameters(5, true, false); |
|||
var gridSensor = (SimpleTestGridSensor)gridSensorComponent.CreateSensors()[0]; |
|||
Assert.AreEqual(gridSensor.PerceptionBuffer.Length, 3 * 4 * 5); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestInvalidSizeConfiguration() |
|||
{ |
|||
testGo.tag = k_Tag2; |
|||
string[] tags = { k_Tag1, k_Tag2 }; |
|||
gridSensorComponent.SetComponentParameters(tags, gridSizeY: 10, useTestingGridSensor: true); |
|||
gridSensorComponent.CreateSensors(); // expect no exception
|
|||
|
|||
gridSensorComponent.m_GridSize.y = 10; |
|||
Assert.Throws<UnityAgentsException>(() => |
|||
{ |
|||
gridSensorComponent.CreateSensors(); |
|||
}); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestInvalidCompressionConfiguration() |
|||
{ |
|||
testGo.tag = k_Tag2; |
|||
string[] tags = { k_Tag1, k_Tag2 }; |
|||
gridSensorComponent.SetComponentParameters(tags, compression: SensorCompressionType.PNG, useTestingGridSensor: true); |
|||
|
|||
var gridSensor = (GridSensorBase)gridSensorComponent.CreateSensors()[0]; |
|||
LogAssert.Expect(LogType.Warning, $"Compression type {SensorCompressionType.PNG} is only supported with normalized data. " + |
|||
"The sensor will not compress the data."); |
|||
Assert.AreEqual(gridSensor.CompressionType, SensorCompressionType.None); |
|||
} |
|||
|
|||
[Test] |
|||
public void PerceiveNotSelf() |
|||
{ |
|||
testGo.tag = k_Tag2; |
|||
|
|||
string[] tags = { k_Tag1, k_Tag2 }; |
|||
gridSensorComponent.SetComponentParameters(tags, useGridSensorBase: true); |
|||
var gridSensor = (GridSensorBase)gridSensorComponent.CreateSensors()[0]; |
|||
|
|||
gridSensor.Update(); |
|||
|
|||
int[] subarrayIndicies = new int[] { 77, 78, 87, 88 }; |
|||
float[][] expectedSubarrays = GridObsTestUtils.DuplicateArray(new float[] { 1 }, 4); |
|||
float[] expectedDefault = new float[] { 0 }; |
|||
GridObsTestUtils.AssertSubarraysAtIndex(gridSensor.PerceptionBuffer, subarrayIndicies, expectedSubarrays, expectedDefault); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestReset() |
|||
{ |
|||
testGo.tag = k_Tag2; |
|||
string[] tags = { k_Tag1, k_Tag2 }; |
|||
gridSensorComponent.SetComponentParameters(tags, useGridSensorBase: true); |
|||
TestGridSensorConfig.SetParameters(3, false, false); |
|||
var gridSensor = (GridSensorBase)gridSensorComponent.CreateSensors()[0]; |
|||
|
|||
gridSensor.Update(); |
|||
|
|||
int[] subarrayIndicies = new int[] { 77, 78, 87, 88 }; |
|||
float[][] expectedSubarrays = GridObsTestUtils.DuplicateArray(new float[] { 1 }, 4); |
|||
float[] expectedDefault = new float[] { 0 }; |
|||
GridObsTestUtils.AssertSubarraysAtIndex(gridSensor.PerceptionBuffer, subarrayIndicies, expectedSubarrays, expectedDefault); |
|||
Object.DestroyImmediate(boxGo); |
|||
|
|||
gridSensor.Update(); |
|||
|
|||
subarrayIndicies = new int[0]; |
|||
expectedSubarrays = new float[0][]; |
|||
GridObsTestUtils.AssertSubarraysAtIndex(gridSensor.PerceptionBuffer, subarrayIndicies, expectedSubarrays, expectedDefault); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestOneHotSensor() |
|||
{ |
|||
testGo.tag = k_Tag2; |
|||
string[] tags = { k_Tag1, k_Tag2 }; |
|||
gridSensorComponent.SetComponentParameters(tags, useOneHotTag: true); |
|||
var gridSensor = (OneHotGridSensor)gridSensorComponent.CreateSensors()[0]; |
|||
Assert.AreEqual(gridSensor.PerceptionBuffer.Length, 10 * 10 * 2); |
|||
|
|||
gridSensor.Update(); |
|||
|
|||
int[] subarrayIndicies = new int[] { 77, 78, 87, 88 }; |
|||
float[][] expectedSubarrays = GridObsTestUtils.DuplicateArray(new float[] { 1, 0 }, 4); |
|||
float[] expectedDefault = new float[] { 0, 0 }; |
|||
GridObsTestUtils.AssertSubarraysAtIndex(gridSensor.PerceptionBuffer, subarrayIndicies, expectedSubarrays, expectedDefault); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestCountingSensor() |
|||
{ |
|||
testGo.tag = k_Tag2; |
|||
string[] tags = { k_Tag1, k_Tag2 }; |
|||
gridSensorComponent.SetComponentParameters(tags, countColliders: true); |
|||
var gridSensor = (CountingGridSensor)gridSensorComponent.CreateSensors()[0]; |
|||
Assert.AreEqual(gridSensor.PerceptionBuffer.Length, 10 * 10 * 2); |
|||
|
|||
gridSensor.Update(); |
|||
|
|||
int[] subarrayIndicies = new int[] { 77, 78, 87, 88 }; |
|||
float[][] expectedSubarrays = GridObsTestUtils.DuplicateArray(new float[] { 1, 0 }, 4); |
|||
float[] expectedDefault = new float[] { 0, 0 }; |
|||
GridObsTestUtils.AssertSubarraysAtIndex(gridSensor.PerceptionBuffer, subarrayIndicies, expectedSubarrays, expectedDefault); |
|||
|
|||
var boxGo2 = new GameObject("block"); |
|||
boxGo2.tag = k_Tag1; |
|||
boxGo2.transform.position = new Vector3(3.1f, 0f, 3f); |
|||
boxGo2.AddComponent<BoxCollider>(); |
|||
|
|||
gridSensor.Update(); |
|||
|
|||
subarrayIndicies = new int[] { 77, 78, 87, 88 }; |
|||
expectedSubarrays = GridObsTestUtils.DuplicateArray(new float[] { 2, 0 }, 4); |
|||
expectedDefault = new float[] { 0, 0 }; |
|||
GridObsTestUtils.AssertSubarraysAtIndex(gridSensor.PerceptionBuffer, subarrayIndicies, expectedSubarrays, expectedDefault); |
|||
Object.DestroyImmediate(boxGo2); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestCustomSensorInvalidData() |
|||
{ |
|||
testGo.tag = k_Tag2; |
|||
string[] tags = { k_Tag1, k_Tag2 }; |
|||
gridSensorComponent.SetComponentParameters(tags, compression: SensorCompressionType.PNG, useTestingGridSensor: true); |
|||
TestGridSensorConfig.SetParameters(5, true, false); |
|||
var gridSensor = (SimpleTestGridSensor)gridSensorComponent.CreateSensors()[0]; |
|||
|
|||
gridSensor.DummyData = new float[] { 1, 2, 3, 4, 5 }; |
|||
Assert.Throws<UnityAgentsException>(() => |
|||
{ |
|||
gridSensor.Update(); |
|||
}); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestMultipleSensors() |
|||
{ |
|||
testGo.tag = k_Tag2; |
|||
string[] tags = { k_Tag1, k_Tag2 }; |
|||
gridSensorComponent.SetComponentParameters(tags, useOneHotTag: true, countColliders: true, useTestingGridSensor: true); |
|||
var gridSensors = gridSensorComponent.CreateSensors(); |
|||
Assert.IsNotNull(((GridSensorBase)gridSensors[0]).m_BoxOverlapChecker); |
|||
Assert.IsNull(((GridSensorBase)gridSensors[1]).m_BoxOverlapChecker); |
|||
Assert.IsNull(((GridSensorBase)gridSensors[2]).m_BoxOverlapChecker); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestNoSensors() |
|||
{ |
|||
testGo.tag = k_Tag2; |
|||
string[] tags = { k_Tag1, k_Tag2 }; |
|||
gridSensorComponent.SetComponentParameters(tags); |
|||
Assert.Throws<UnityAgentsException>(() => |
|||
{ |
|||
gridSensorComponent.CreateSensors(); |
|||
}); |
|||
} |
|||
} |
|||
} |
|
|||
using System.Linq; |
|||
using UnityEngine; |
|||
using Unity.MLAgents.Sensors; |
|||
using Unity.MLAgents.Extensions.Sensors; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Tests.GridSensors |
|||
{ |
|||
public static class TestGridSensorConfig |
|||
{ |
|||
public static int ObservationSize; |
|||
public static bool IsNormalized; |
|||
public static bool ParseAllColliders; |
|||
|
|||
public static void SetParameters(int observationSize, bool isNormalized, bool parseAllColliders) |
|||
{ |
|||
ObservationSize = observationSize; |
|||
IsNormalized = isNormalized; |
|||
ParseAllColliders = parseAllColliders; |
|||
} |
|||
|
|||
public static void Reset() |
|||
{ |
|||
ObservationSize = 0; |
|||
IsNormalized = false; |
|||
ParseAllColliders = false; |
|||
} |
|||
} |
|||
|
|||
public class SimpleTestGridSensor : GridSensorBase |
|||
{ |
|||
public float[] DummyData; |
|||
|
|||
public SimpleTestGridSensor( |
|||
string name, |
|||
Vector3 cellScale, |
|||
Vector3Int gridSize, |
|||
string[] detectableTags, |
|||
SensorCompressionType compression |
|||
) : base( |
|||
name, |
|||
cellScale, |
|||
gridSize, |
|||
detectableTags, |
|||
compression) |
|||
{ } |
|||
|
|||
protected override int GetCellObservationSize() |
|||
{ |
|||
return TestGridSensorConfig.ObservationSize; |
|||
} |
|||
|
|||
protected override bool IsDataNormalized() |
|||
{ |
|||
return TestGridSensorConfig.IsNormalized; |
|||
} |
|||
|
|||
protected internal override ProcessCollidersMethod GetProcessCollidersMethod() |
|||
{ |
|||
return TestGridSensorConfig.ParseAllColliders ? ProcessCollidersMethod.ProcessAllColliders : ProcessCollidersMethod.ProcessClosestColliders; |
|||
} |
|||
protected override void GetObjectData(GameObject detectedObject, int typeIndex, float[] dataBuffer) |
|||
{ |
|||
for (var i = 0; i < DummyData.Length; i++) |
|||
{ |
|||
dataBuffer[i] = DummyData[i]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public class SimpleTestGridSensorComponent : GridSensorComponent |
|||
{ |
|||
bool m_UseTestingGridSensor; |
|||
bool m_UseGridSensorBase; |
|||
|
|||
protected override GridSensorBase[] GetGridSensors() |
|||
{ |
|||
var sensorList = base.GetGridSensors().ToList(); |
|||
if (m_UseGridSensorBase) |
|||
{ |
|||
var testSensor = new GridSensorBase( |
|||
SensorName, |
|||
CellScale, |
|||
GridSize, |
|||
DetectableTags, |
|||
CompressionType |
|||
); |
|||
sensorList.Add(testSensor); |
|||
} |
|||
if (m_UseTestingGridSensor) |
|||
{ |
|||
var testSensor = new SimpleTestGridSensor( |
|||
SensorName, |
|||
CellScale, |
|||
GridSize, |
|||
DetectableTags, |
|||
CompressionType |
|||
); |
|||
sensorList.Add(testSensor); |
|||
} |
|||
return sensorList.ToArray(); |
|||
} |
|||
|
|||
public void SetComponentParameters( |
|||
string[] detectableTags = null, |
|||
float cellScaleX = 1f, |
|||
float cellScaleZ = 1f, |
|||
int gridSizeX = 10, |
|||
int gridSizeY = 1, |
|||
int gridSizeZ = 10, |
|||
int colliderMaskInt = -1, |
|||
SensorCompressionType compression = SensorCompressionType.None, |
|||
bool rotateWithAgent = false, |
|||
bool useOneHotTag = false, |
|||
bool countColliders = false, |
|||
bool useTestingGridSensor = false, |
|||
bool useGridSensorBase = false |
|||
) |
|||
{ |
|||
DetectableTags = detectableTags; |
|||
CellScale = new Vector3(cellScaleX, 0.01f, cellScaleZ); |
|||
GridSize = new Vector3Int(gridSizeX, gridSizeY, gridSizeZ); |
|||
ColliderMask = colliderMaskInt < 0 ? LayerMask.GetMask("Default") : colliderMaskInt; |
|||
RotateWithAgent = rotateWithAgent; |
|||
CompressionType = compression; |
|||
UseOneHotTag = useOneHotTag; |
|||
CountColliders = countColliders; |
|||
m_UseGridSensorBase = useGridSensorBase; |
|||
m_UseTestingGridSensor = useTestingGridSensor; |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.CompilerServices; |
|||
using UnityEngine; |
|||
using UnityEngine.Assertions; |
|||
using Unity.MLAgents.Sensors; |
|||
using UnityEngine.Profiling; |
|||
|
|||
[assembly: InternalsVisibleTo("Unity.ML-Agents.Extensions.EditorTests")] |
|||
namespace Unity.MLAgents.Extensions.Sensors |
|||
{ |
|||
/// <summary>
|
|||
/// Enum describing what kind of depth type the data should be organized as
|
|||
/// </summary>
|
|||
public enum GridDepthType { Channel, ChannelHot, Counting }; |
|||
|
|||
/// <summary>
|
|||
/// Grid-based sensor.
|
|||
/// </summary>
|
|||
public class GridSensor : ISensor, IBuiltInSensor |
|||
{ |
|||
string m_Name; |
|||
Vector3 m_CellScale; |
|||
Vector3Int m_GridSize; |
|||
bool m_RotateWithAgent; |
|||
GameObject m_RootReference; |
|||
int m_MaxColliderBufferSize; |
|||
int m_InitialColliderBufferSize; |
|||
LayerMask m_ColliderMask; |
|||
GridDepthType m_GridDepthType; |
|||
int[] m_ChannelDepths; |
|||
string[] m_DetectableObjects; |
|||
SensorCompressionType m_CompressionType; |
|||
ObservationSpec m_ObservationSpec; |
|||
|
|||
// Buffers
|
|||
internal float[] m_PerceptionBuffer; |
|||
Color[] m_PerceptionColors; |
|||
Texture2D m_PerceptionTexture; |
|||
Collider[] m_ColliderBuffer; |
|||
float[] m_CellDataBuffer; |
|||
int[] m_ChannelOffsets; |
|||
Vector3[] m_CellLocalPositions; |
|||
int[] m_GizmoColorIndexes; |
|||
Vector3[] m_CellGlobalPosition; |
|||
|
|||
// Utility Constants Calculated on Init
|
|||
int m_NumCells; |
|||
int m_CellObservationSize; |
|||
Vector3 m_CellCenterOffset; |
|||
|
|||
|
|||
public GridSensor( |
|||
string name, |
|||
Vector3 cellScale, |
|||
Vector3Int gridNum, |
|||
bool rotateWithAgent, |
|||
int[] channelDepths, |
|||
string[] detectableObjects, |
|||
LayerMask colliderMask, |
|||
GridDepthType depthType, |
|||
GameObject rootReference, |
|||
SensorCompressionType compression, |
|||
int maxColliderBufferSize, |
|||
int initialColliderBufferSize |
|||
) |
|||
{ |
|||
m_Name = name; |
|||
m_CellScale = cellScale; |
|||
m_GridSize = gridNum; |
|||
m_RotateWithAgent = rotateWithAgent; |
|||
m_RootReference = rootReference; |
|||
m_MaxColliderBufferSize = maxColliderBufferSize; |
|||
m_InitialColliderBufferSize = initialColliderBufferSize; |
|||
m_ColliderMask = colliderMask; |
|||
m_GridDepthType = depthType; |
|||
m_ChannelDepths = channelDepths; |
|||
m_DetectableObjects = detectableObjects; |
|||
m_CompressionType = compression; |
|||
|
|||
if (m_GridSize.y != 1) |
|||
{ |
|||
throw new UnityAgentsException("GridSensor only supports 2D grids."); |
|||
} |
|||
|
|||
if (m_GridDepthType == GridDepthType.Counting && m_DetectableObjects.Length != m_ChannelDepths.Length) |
|||
{ |
|||
throw new UnityAgentsException("The channels of a CountingGridSensor is equal to the number of detectableObjects"); |
|||
} |
|||
|
|||
InitGridParameters(); |
|||
InitDepthType(); |
|||
InitCellPoints(); |
|||
ResetPerceptionBuffer(); |
|||
|
|||
m_ObservationSpec = ObservationSpec.Visual(m_GridSize.x, m_GridSize.z, m_CellObservationSize); |
|||
m_PerceptionTexture = new Texture2D(m_GridSize.x, m_GridSize.z, TextureFormat.RGB24, false); |
|||
m_ColliderBuffer = new Collider[Math.Min(m_MaxColliderBufferSize, m_InitialColliderBufferSize)]; |
|||
} |
|||
|
|||
public SensorCompressionType CompressionType |
|||
{ |
|||
get { return m_CompressionType; } |
|||
set { m_CompressionType = value; } |
|||
} |
|||
|
|||
public bool RotateWithAgent |
|||
{ |
|||
get { return m_RotateWithAgent; } |
|||
set { m_RotateWithAgent = value; } |
|||
} |
|||
|
|||
public LayerMask ColliderMask |
|||
{ |
|||
get { return m_ColliderMask; } |
|||
set { m_ColliderMask = value; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes the constant parameters used within the perceive method call
|
|||
/// </summary>
|
|||
void InitGridParameters() |
|||
{ |
|||
m_NumCells = m_GridSize.x * m_GridSize.z; |
|||
m_CellCenterOffset = new Vector3((m_GridSize.x - 1f) / 2, 0, (m_GridSize.z - 1f) / 2); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes the constant parameters that are based on the Grid Depth Type
|
|||
/// Sets the ObservationPerCell and the ChannelOffsets properties
|
|||
/// </summary>
|
|||
void InitDepthType() |
|||
{ |
|||
if (m_GridDepthType == GridDepthType.ChannelHot) |
|||
{ |
|||
m_CellObservationSize = m_ChannelDepths.Sum(); |
|||
|
|||
m_ChannelOffsets = new int[m_ChannelDepths.Length]; |
|||
for (int i = 1; i < m_ChannelDepths.Length; i++) |
|||
{ |
|||
m_ChannelOffsets[i] = m_ChannelOffsets[i - 1] + m_ChannelDepths[i - 1]; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
m_CellObservationSize = m_ChannelDepths.Length; |
|||
} |
|||
|
|||
// 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(m_CellObservationSize < (255 * 3), "The maximum number of channels per cell must be less than 255 * 3"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes the location of the CellPoints property
|
|||
/// </summary>
|
|||
void InitCellPoints() |
|||
{ |
|||
m_CellLocalPositions = new Vector3[m_NumCells]; |
|||
|
|||
for (int i = 0; i < m_NumCells; i++) |
|||
{ |
|||
m_CellLocalPositions[i] = CellToLocalPoint(i); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Reset() { } |
|||
|
|||
/// <summary>
|
|||
/// 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)
|
|||
/// </summary>
|
|||
public void ResetPerceptionBuffer() |
|||
{ |
|||
if (m_PerceptionBuffer != null) |
|||
{ |
|||
Array.Clear(m_PerceptionBuffer, 0, m_PerceptionBuffer.Length); |
|||
} |
|||
else |
|||
{ |
|||
m_PerceptionBuffer = new float[m_CellObservationSize * m_NumCells]; |
|||
m_ColliderBuffer = new Collider[Math.Min(m_MaxColliderBufferSize, m_InitialColliderBufferSize)]; |
|||
m_CellDataBuffer = new float[m_ChannelDepths.Length]; |
|||
m_PerceptionColors = new Color[m_NumCells]; |
|||
m_CellGlobalPosition = new Vector3[m_NumCells]; |
|||
} |
|||
} |
|||
|
|||
public void ResetGizmoBuffer() |
|||
{ |
|||
// Ensure to init arrays if not yet assigned (for editor)
|
|||
if (m_GizmoColorIndexes == null) |
|||
m_GizmoColorIndexes = new int[m_NumCells]; |
|||
|
|||
// Assign the default color to the cell activities
|
|||
for (int i = 0; i < m_NumCells; i++) |
|||
{ |
|||
m_GizmoColorIndexes[i] = -1; |
|||
} |
|||
} |
|||
|
|||
|
|||
/// <inheritdoc/>
|
|||
public string GetName() |
|||
{ |
|||
return m_Name; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public CompressionSpec GetCompressionSpec() |
|||
{ |
|||
return new CompressionSpec(CompressionType); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public BuiltInSensorType GetBuiltInSensorType() |
|||
{ |
|||
return BuiltInSensorType.GridSensor; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 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
|
|||
/// </summary>
|
|||
/// <returns>byte[] containing the compressed observation of the grid observation</returns>
|
|||
public byte[] GetCompressedObservation() |
|||
{ |
|||
using (TimerStack.Instance.Scoped("GridSensor.GetCompressedObservation")) |
|||
{ |
|||
var allBytes = new List<byte>(); |
|||
var numImages = (m_CellObservationSize + 2) / 3; |
|||
for (int i = 0; i < numImages; i++) |
|||
{ |
|||
var channelIndex = 3 * i; |
|||
ChannelsToTexture(channelIndex, Math.Min(3, m_CellObservationSize - channelIndex)); |
|||
allBytes.AddRange(m_PerceptionTexture.EncodeToPNG()); |
|||
} |
|||
|
|||
return allBytes.ToArray(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 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
|
|||
/// </summary>
|
|||
/// <param name="channelIndex"></param>
|
|||
/// <param name="numChannelsToAdd"></param>
|
|||
void ChannelsToTexture(int channelIndex, int numChannelsToAdd) |
|||
{ |
|||
for (int i = 0; i < m_NumCells; i++) |
|||
{ |
|||
for (int j = 0; j < numChannelsToAdd; j++) |
|||
{ |
|||
m_PerceptionColors[i][j] = m_PerceptionBuffer[i * m_CellObservationSize + channelIndex + j]; |
|||
} |
|||
} |
|||
m_PerceptionTexture.SetPixels(m_PerceptionColors); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Perceive - Clears the buffers, calls overlap box on the actual cell (the actual perception part)
|
|||
/// for all found colliders, LoadObjectData is called
|
|||
/// </summary>
|
|||
internal void Perceive() |
|||
{ |
|||
if (m_ColliderBuffer == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
ResetPerceptionBuffer(); |
|||
using (TimerStack.Instance.Scoped("GridSensor.Perceive")) |
|||
{ |
|||
var halfCellScale = new Vector3(m_CellScale.x / 2f, m_CellScale.y, m_CellScale.z / 2f); |
|||
|
|||
for (var cellIndex = 0; cellIndex < m_NumCells; cellIndex++) |
|||
{ |
|||
var cellCenter = GetCellGlobalPosition(cellIndex); |
|||
var numFound = BufferResizingOverlapBoxNonAlloc(cellCenter, halfCellScale, GetGridRotation()); |
|||
|
|||
if (numFound > 0) |
|||
{ |
|||
if (m_GridDepthType == GridDepthType.Counting) |
|||
{ |
|||
ParseCollidersAll(m_ColliderBuffer, numFound, cellIndex, cellCenter); |
|||
} |
|||
else |
|||
{ |
|||
ParseCollidersClosest(m_ColliderBuffer, numFound, cellIndex, cellCenter); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 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.
|
|||
/// </summary>
|
|||
/// <param name="cellCenter"></param>
|
|||
/// <param name="halfCellScale"></param>
|
|||
/// <param name="rotation"></param>
|
|||
/// <returns></returns>
|
|||
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, m_ColliderMask); |
|||
if (numFound == m_ColliderBuffer.Length && m_ColliderBuffer.Length < m_MaxColliderBufferSize) |
|||
{ |
|||
m_ColliderBuffer = new Collider[Math.Min(m_MaxColliderBufferSize, m_ColliderBuffer.Length * 2)]; |
|||
m_InitialColliderBufferSize = m_ColliderBuffer.Length; |
|||
} |
|||
else |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return numFound; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Parses the array of colliders found within a cell. Finds the closest gameobject to the agent root reference within the cell
|
|||
/// </summary>
|
|||
/// <param name="foundColliders">Array of the colliders found within the cell</param>
|
|||
/// <param name="numFound">Number of colliders found.</param>
|
|||
/// <param name="cellIndex">The index of the cell</param>
|
|||
/// <param name="cellCenter">The center position of the cell</param>
|
|||
void ParseCollidersClosest(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, m_RootReference)) |
|||
continue; |
|||
|
|||
var closestColliderPoint = foundColliders[i].ClosestPointOnBounds(cellCenter); |
|||
var currentDistanceSquared = (closestColliderPoint - m_RootReference.transform.position).sqrMagnitude; |
|||
|
|||
// Checks if our colliders contain a detectable object
|
|||
var index = -1; |
|||
for (var ii = 0; ii < m_DetectableObjects.Length; ii++) |
|||
{ |
|||
if (currentColliderGo.CompareTag(m_DetectableObjects[ii])) |
|||
{ |
|||
index = ii; |
|||
break; |
|||
} |
|||
} |
|||
if (index > -1 && currentDistanceSquared < minDistanceSquared) |
|||
{ |
|||
minDistanceSquared = currentDistanceSquared; |
|||
closestColliderGo = currentColliderGo; |
|||
} |
|||
} |
|||
|
|||
if (!ReferenceEquals(closestColliderGo, null)) |
|||
{ |
|||
LoadObjectData(closestColliderGo, cellIndex); |
|||
} |
|||
Profiler.EndSample(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// For each collider, calls LoadObjectData on the gameobejct
|
|||
/// </summary>
|
|||
/// <param name="foundColliders">The array of colliders</param>
|
|||
/// <param name="cellIndex">The cell index the collider is in</param>
|
|||
/// <param name="cellCenter">the center of the cell the collider is in</param>
|
|||
void ParseCollidersAll(Collider[] foundColliders, int numFound, int cellIndex, Vector3 cellCenter) |
|||
{ |
|||
Profiler.BeginSample("GridSensor.ParseColliders"); |
|||
GameObject currentColliderGo = null; |
|||
Vector3 closestColliderPoint = Vector3.zero; |
|||
|
|||
for (int i = 0; i < numFound; i++) |
|||
{ |
|||
currentColliderGo = foundColliders[i].gameObject; |
|||
|
|||
// Continue if the current collider go is the root reference
|
|||
if (currentColliderGo == m_RootReference) |
|||
continue; |
|||
|
|||
closestColliderPoint = foundColliders[i].ClosestPointOnBounds(cellCenter); |
|||
|
|||
LoadObjectData(currentColliderGo, cellIndex); |
|||
} |
|||
Profiler.EndSample(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 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.
|
|||
/// </summary>
|
|||
/// <returns>
|
|||
/// A float[] containing the data that holds the representative information of the passed in gameObject
|
|||
/// </returns>
|
|||
/// <param name="currentColliderGo">The game object that was found colliding with a certain cell</param>
|
|||
/// <param name="typeIndex">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)</param>
|
|||
/// <param name="normalizedDistance">A float between 0 and 1 describing the ratio of
|
|||
/// the distance currentColliderGo is compared to the edge of the gridsensor</param>
|
|||
/// <example>
|
|||
/// Here is an example of extenind GetObjectData to include information about a potential Rigidbody:
|
|||
/// <code>
|
|||
/// 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;
|
|||
/// }
|
|||
/// </code>
|
|||
/// </example>
|
|||
protected virtual float[] GetObjectData(GameObject currentColliderGo, int typeIndex) |
|||
{ |
|||
Array.Clear(m_CellDataBuffer, 0, m_CellDataBuffer.Length); |
|||
m_CellDataBuffer[0] = typeIndex; |
|||
return m_CellDataBuffer; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Runs basic validation assertions to check that the values can be normalized
|
|||
/// </summary>
|
|||
/// <param name="channelValues">The values to be validated</param>
|
|||
/// <param name="currentColliderGo">The gameobject used for better error messages</param>
|
|||
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] > m_ChannelDepths[j]) |
|||
throw new UnityAgentsException("Expected ChannelValue[" + j + "] for " + currentColliderGo.name + " to be less than ChannelDepth[" + j + "] (" + m_ChannelDepths[j] + "), was " + channelValues[j]); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 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
|
|||
/// </summary>
|
|||
/// <param name="currentColliderGo">The game object that was found colliding with a certain cell</param>
|
|||
/// <param name="cellIndex">The index of the current cell</param>
|
|||
protected virtual void LoadObjectData(GameObject currentColliderGo, int cellIndex) |
|||
{ |
|||
Profiler.BeginSample("GridSensor.LoadObjectData"); |
|||
var channelHotVals = new ArraySegment<float>(m_PerceptionBuffer, cellIndex * m_CellObservationSize, m_CellObservationSize); |
|||
for (var i = 0; i < m_DetectableObjects.Length; i++) |
|||
{ |
|||
if (!ReferenceEquals(currentColliderGo, null) && currentColliderGo.CompareTag(m_DetectableObjects[i])) |
|||
{ |
|||
// TODO: Create the array already then set the values using "out" in GetObjectData
|
|||
var channelValues = GetObjectData(currentColliderGo, i); |
|||
ValidateValues(channelValues, currentColliderGo); |
|||
|
|||
switch (m_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] /= m_ChannelDepths[j]; |
|||
} |
|||
|
|||
Array.Copy(channelValues, 0, m_PerceptionBuffer, cellIndex * m_CellObservationSize, m_CellObservationSize); |
|||
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 (m_ChannelDepths[j] > 1) |
|||
{ |
|||
m_PerceptionBuffer[channelHotVals.Offset + (int)channelValues[j] + m_ChannelOffsets[j]] = 1f; |
|||
} |
|||
else |
|||
{ |
|||
m_PerceptionBuffer[channelHotVals.Offset + m_ChannelOffsets[j]] = channelValues[j]; |
|||
} |
|||
} |
|||
break; |
|||
} |
|||
case GridDepthType.Counting: |
|||
{ |
|||
// The observations are "channel count" so each grid is WxHxC where C is the number of tags
|
|||
// This means that each value channelValues[i] is a counter of gameobject included into grid cells
|
|||
// where i is the index of the tag in DetectableObjects
|
|||
int countIndex = cellIndex * m_CellObservationSize + i; |
|||
m_PerceptionBuffer[countIndex] = Mathf.Min(1f, m_PerceptionBuffer[countIndex] + 1f / m_ChannelDepths[i]); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
Profiler.EndSample(); |
|||
} |
|||
|
|||
/// <summary>Converts the index of the cell to the 3D point (y is zero) relative to grid center</summary>
|
|||
/// <returns>Vector3 of the position of the center of the cell relative to grid center</returns>
|
|||
/// <param name="cell">The index of the cell</param>
|
|||
Vector3 CellToLocalPoint(int cellIndex) |
|||
{ |
|||
float x = (cellIndex / m_GridSize.z - m_CellCenterOffset.x) * m_CellScale.x; |
|||
float z = (cellIndex % m_GridSize.z - m_CellCenterOffset.z) * m_CellScale.z; |
|||
return new Vector3(x, 0, z); |
|||
} |
|||
|
|||
internal Vector3 GetCellGlobalPosition(int cellIndex) |
|||
{ |
|||
if (m_RotateWithAgent) |
|||
{ |
|||
return m_RootReference.transform.TransformPoint(m_CellLocalPositions[cellIndex]); |
|||
} |
|||
else |
|||
{ |
|||
return m_CellLocalPositions[cellIndex] + m_RootReference.transform.position; |
|||
} |
|||
} |
|||
|
|||
internal Quaternion GetGridRotation() |
|||
{ |
|||
return m_RotateWithAgent ? m_RootReference.transform.rotation : Quaternion.identity; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Update() |
|||
{ |
|||
using (TimerStack.Instance.Scoped("GridSensor.Update")) |
|||
{ |
|||
Perceive(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public ObservationSpec GetObservationSpec() |
|||
{ |
|||
return m_ObservationSpec; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public int Write(ObservationWriter writer) |
|||
{ |
|||
using (TimerStack.Instance.Scoped("GridSensor.Write")) |
|||
{ |
|||
int index = 0; |
|||
for (var h = m_GridSize.z - 1; h >= 0; h--) |
|||
{ |
|||
for (var w = 0; w < m_GridSize.x; w++) |
|||
{ |
|||
for (var d = 0; d < m_CellObservationSize; d++) |
|||
{ |
|||
writer[h, w, d] = m_PerceptionBuffer[index]; |
|||
index++; |
|||
} |
|||
} |
|||
} |
|||
return index; |
|||
} |
|||
} |
|||
|
|||
internal int[] PerceiveGizmoColor() |
|||
{ |
|||
ResetGizmoBuffer(); |
|||
var halfCellScale = new Vector3(m_CellScale.x / 2f, m_CellScale.y, m_CellScale.z / 2f); |
|||
|
|||
for (var cellIndex = 0; cellIndex < m_NumCells; cellIndex++) |
|||
{ |
|||
var cellCenter = GetCellGlobalPosition(cellIndex); |
|||
var numFound = BufferResizingOverlapBoxNonAlloc(cellCenter, halfCellScale, GetGridRotation()); |
|||
|
|||
var minDistanceSquared = float.MaxValue; |
|||
var tagIndex = -1; |
|||
|
|||
for (var i = 0; i < numFound; i++) |
|||
{ |
|||
var currentColliderGo = m_ColliderBuffer[i].gameObject; |
|||
if (ReferenceEquals(currentColliderGo, m_RootReference)) |
|||
continue; |
|||
|
|||
var closestColliderPoint = m_ColliderBuffer[i].ClosestPointOnBounds(cellCenter); |
|||
var currentDistanceSquared = (closestColliderPoint - m_RootReference.transform.position).sqrMagnitude; |
|||
|
|||
// Checks if our colliders contain a detectable object
|
|||
var index = -1; |
|||
for (var ii = 0; ii < m_DetectableObjects.Length; ii++) |
|||
{ |
|||
if (currentColliderGo.CompareTag(m_DetectableObjects[ii])) |
|||
{ |
|||
index = ii; |
|||
break; |
|||
} |
|||
} |
|||
if (index > -1 && currentDistanceSquared < minDistanceSquared) |
|||
{ |
|||
minDistanceSquared = currentDistanceSquared; |
|||
tagIndex = index; |
|||
} |
|||
} |
|||
m_GizmoColorIndexes[cellIndex] = tagIndex; |
|||
} |
|||
return m_GizmoColorIndexes; |
|||
} |
|||
|
|||
internal Vector3[] GetGizmoPositions() |
|||
{ |
|||
for (var i = 0; i < m_NumCells; i++) |
|||
{ |
|||
m_CellGlobalPosition[i] = GetCellGlobalPosition(i); |
|||
} |
|||
return m_CellGlobalPosition; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: b7ffdca5cd8064ee6831175d7ffd3f0f |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 8cb560e251b28433385e46b2b75d6dae |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
撰写
预览
正在加载...
取消
保存
Reference in new issue