3. Scripting

Many scripts in AGX Dynamics for Unity are an additional API layer on top of the AGX Dynamics API, managing data and additional features suitable for the Unity editor. The purpose of the additional data layer is mainly for serialization and transformation from/to Unity’s left to AGX Dynamics right handed coordinate frames.

The root namespace of runtime classes is AGXUnity and AGXUnityEditor for editor classes. The complete AGX Dynamics API is available in Unity and is accessed using namespaces agx, agxCollide, agxSDK, agxModel, agxTerrain, agxVehicle, agxHydraulics, agxPowerLine, agxDriveTrain, to name a few.

Instances of AGX Dynamics types are available during runtime. If instances are created within the editor loop (i.e., play isn’t pressed), it’s important that the lifetime of the instances are as short as possible and are properly disposed before the editor changes state.

An example usage of an agx.RigidBody instance within the editor loop:

using ( var rigidBody = new agx.RigidBody() ) {
  ...
}

// Or similarly:

var rigidBody = new agx.RigidBody();

...

rigidBody.Dispose();
rigidBody = null;

The same pattern should be applied in runtime scripts. E.g.,

public class MyScript : UnityEngine.MonoBehaviour
{
  private agx.RigidBody m_rigidBody = null;

  private void Start()
  {
    m_rigidBody = new agx.RigidBody();
  }

  private void OnDestroy()
  {
    if ( m_rigidBody == null )
      return;

    m_rigidBody.Dispose();
    m_rigidBody = null;
  }
}

3.1. AGXUnity.ScriptComponent

AGXUnity.ScriptComponent is extending UnityEngine.MonoBehaviour and implements additional features, such as initialization and propagation of data. It’s implicit that classes of AGXUnity.ScriptComponent are dependent on and/or are managing native AGX Dynamics instances. They normally have a property Native to access the native instance for full access of the AGX Dynamics API of that object.

3.1.1. Initialization of native instances

The native instances are normally created when Unity calls Start of the components of a GameObject - if nothing else depends on the component. Instead of Start, each AGXUnity.ScriptComponent implements protected override bool Initialize() which enables the native instances to be initialized in e.g., Awake or Start of another script. To trigger initialization or get an already initialized instance, call GetInitialized<T>(). If the initialization, for some reason, failed, null is returned.

The following example is assigning a tag to the native instance of a Rigid Body in the Awake callback of the script:

using UnityEngine;
using AGXUnity;

namespace Scripts
{
  public class MyScript : MonoBehaviour
  {
    private void Awake()
    {
      var rb = GetComponent<RigidBody>()?.GetInitialized<RigidBody>();
      // Successfully initialized, implicit that rb.Native != null.
      if ( rb != null )
        rb.Native.getPropertyContainer().addPropertyBool( "tag", true );
    }
  }
}

3.1.2. Native data synchronization

All data is synchronized to the native instance when the AGXUnity.ScriptComponent has been initialized. The initial synchronization is performed by matching field names with property names of the type being initialized.

Consider a custom script (inheriting from AGXUnity.ScriptComponent) implementing some functionality involving an agx.RigidBody. The custom script includes properties Native, Mass and LinearVelocity:

using UnityEngine;
using AGXUnity;
using AGXUnity.Utils; // For extension Vector3 -> agx.Vec3.

namespace Scripts
{
  public class MyRigidBody : ScriptComponent
  {
    public agx.RigidBody Native { get; private set; } = null;

    [SerializeField]
    private float m_mass = 1.0f;

    public float Mass
    {
      get { return m_mass; }
      set
      {
        m_mass = value;
        if ( Native != null ) {
          Debug.Log( "Setting mass: " + m_mass );
          Native.getMassProperties().setMass( m_mass );
        }
      }
    }

    [SerializeField]
    private Vector3 m_linearVelocity = Vector3.zero;

    public Vector3 LinearVelocity
    {
      get { return m_linearVelocity; }
      set
      {
        m_linearVelocity = value;
        if ( Native != null ) {
          Debug.Log( "Setting velocity: " + m_linearVelocity );

          // Converts from left handed Vector3 velocity to right
          // handed agx.Vec3 velocity.
          Native.setVelocity( m_linearVelocity.ToHandedVec3() );
        }
      }
    }

    protected override bool Initialize()
    {
      Native = new agx.RigidBody( name );
      Native.add( new agxCollide.Geometry( new agxCollide.Sphere( 0.5 ) ) );
      GetSimulation().add( Native );

      Debug.Log( "Initialize success." );

      return true;
    }

    protected override void OnDestroy()
    {
      if ( Simulation.HasInstance )
        GetSimulation().remove( Native );

      Native.Dispose();
      Native = null;

      base.OnDestroy();
    }
  }
}

In an empty scene, create a new empty GameObject, add the MyRigidBody component, change mass to 5 and linear velocity to (1, 2, 3), we get the following log when pressing play:

_images/scripting_myrigidbody_inspector.png

Our custom script has two matching fields and properties, namely m_mass <-> Mass and m_linearVelocity <-> LinearVelocity, being invoked after Initialize returns true. The post-initialize synchronization is basically performing:

// instance.<Name> = instance.m_<name>
instance.Mass = instance.m_mass;
instance.LinearVelocity = instance.m_linearVelocity;

using reflection.

Note

Unity isn’t serializing private fields of components so the [SerializeField] attribute has to be added to all private fields with matching properties for the value to be serialized.

It’s important and convenient to have the synchronization at one place, minimizing bugs related to data not being propagated to the simulation. Our current implementation fully supports scripts changing values of our component, e.g.,

GetComponent<MyRigidBody>().Mass = 500.0f; // Will write to Native IF created.

It’s also possible to change values in the Inspector when the editor isn’t playing. The fields and properties synchronization takes place once, directly after initialization, and after that it’s important that the public properties of our class are used for the data to be written to our native instance. Unity’s default Inspector Editor is showing our private fields m_mass and m_linearVelocity, by removing m_, capital first character and splitting words given camel case. E.g., adding private field:

[SerializeField]
private int m_thisIsATestField = 0;

without any public property will show up in the inspector as:

_images/scripting_myrigidbody_field_inspector.png

It’s hence not possible to use Unity’s default Inspector Editor if we require changes to propagate to our native instance while the editor is playing. Most components and scriptable objects in AGX Dynamics for Unity are instead using a custom Inspector Editor, rendering any public property or field in the Inspector. If enabled for our class, unlike Unity’s default, our properties Mass and LinearVelocity will be rendered instead, and changes will show up as value in our property set methods. Basically, our Mass property is rendered in the Inspector as:

instance.Mass = UnityEditor.EditorGUILayout.FloatField( "Mass", instance.Mass );

To enable AGX Dynamics for Unity custom Inspector Editor, simply add a class inheriting from AGXUnityEditor.InspectorEditor, with the [UnityEditor.CustomEditor( typeof( MyRigidBody ) )] attribute. E.g.,

#if UNITY_EDITOR
namespace Scripts.Editor
{
  [UnityEditor.CustomEditor( typeof( MyRigidBody ) )]
  public class MyRigidBodyEditor : AGXUnityEditor.InspectorEditor
  {
  }
}
#endif

will result in the Inspector of MyRigidBody to become:

_images/scripting_myrigidbody_agxunity_inspector.png

Making changes during runtime will print our logs within the if ( Native != null ) blocks and the private field This Is A Test Field isn’t shown anymore when only publicly accessible fields and properties are rendered.

3.2. AGXUnity.ScriptAsset

AGXUnity.ScriptAsset is extending UnityEngine.ScriptableObject and is assumed to be data on disk but adopts the behavior of AGXUnity.ScriptComponent, managing and synchronizing native instances. Similar to AGXUnity.ScriptComponent, any class inheriting from this class implements Initialize and usage requires GetInitialized<T>() for access to its native instance. Fields and properties are also synchronized after a successful initialization.

While in the editor, changes made to AGXUnity.ScriptAsset references during runtime will not be reverted when the editor is stopped. This is the default behavior of Unity for UnityEngine.ScriptableObject.

3.3. Simulation Callbacks

When the simulation is stepping, there are specific times when data should be read/written and specific data is available. E.g., collision detection has to be done for contact data to be available and the solver has to be done for constraint and contact forces to be available.

The figure below summarizes the available callbacks and the order in which they are invoked during a step.

_images/events_order.png

Callback names and order in which they are invoked. See Step Callbacks and Contact Callbacks for details of each individual callback.

3.3.1. Step Callbacks

Step callbacks are callbacks made during the simulation step. In order:

  1. PreStepForward: First callback from within AGXUnity.Simulation. This callback can, e.g., be used to change initial conditions for the current step.

  2. PreSynchronizeTransforms: Callback from within AGXUnity.Simulation. This callback is used to write current transforms to native instances.

  3. SimulationPreCollide: Callback from within the native agxSDK.Simulation step, before collision detection is performed. This callback can, e.g., be used to do final changes that collision detection depends on.

  4. SimulationPre: Callback from within the native agxSDK.Simulation step, before the dynamics solvers are solving the system. This callback can, e.g., be used to check and modify contact data (more in Contact Callbacks) or change initial conditions for the solver, such as constraint compliance and damping.

  5. SimulationPost: Callback from within the native agxSDK.Simulation step, after the dynamics has been solved and all moving objects has new transforms. Note that their Unity counterparts hasn’t updated their transforms yet. This callback can, e.g., be used to monitor data, such as constraint/contact forces and torques, from the solve.

  6. SimulationLast: Callback from within the native agxSDK.Simulation step, last thing that occurs before the native step is done. This callback can, e.g., be used to summarize simulation data gathered in many post callbacks.

  7. PostSynchronizeTransforms: Callback from within AGXUnity.Simulation. This callback is used to read transforms from the native simulation, writing them to the corresponding Unity transforms.

  8. PostStepForward: Callback from within AGXUnity.Simulation. Last callback before the simulation step is done where all data can be assumed to be up to date in Unity.

All step callbacks signatures are void Callback() and to assign to a callback, e.g., from a UnityEngine.MonoBehaviour script, do:

private void OnEnable()
{
  Simulation.Instance.StepCallbacks.PreStepForward += () =>
  {
    Debug.Log( "Inline: " + Simulation.Instance.Native.getTimeStamp() );
  };

  Simulation.Instance.StepCallbacks.PreStepForward += OnPreStepForward;
}

private void OnPreStepForward()
{
  Debug.Log( "OnPreStepForward: " + Simulation.Instance.Native.getTimeStamp() );
}

Use -= to remove a callback.

3.3.1.1. Constraint force collector

An example script, ConstraintForceCollector.cs, collecting forces and torques applied to the one or two rigid bodies involved in a constraint. The script is collecting forces in SimulationPost so the fresh data is available in SimulationLast or PostStepForward.

using System.Collections.Generic;
using UnityEngine;
using AGXUnity;
using AGXUnity.Utils;

namespace Scripts
{
  public class ConstraintForceCollector : MonoBehaviour
  {
    public struct ForceData
    {
      public float Time;

      public Vector3 RigidBody1Force;
      public Vector3 RigidBody2Force;

      public Vector3 RigidBody1Torque;
      public Vector3 RigidBody2Torque;
    }

    public Constraint Constraint = null;

    public ForceData[] Data => m_stepForceData?.ToArray() ?? new ForceData[] { };

    private void Start()
    {
      if ( Constraint == null ) {
        Debug.LogError( "No constraint assigned.", this );
        return;
      }

      // If initialization fails the error will be logged from
      // the constraint.
      if ( Constraint.GetInitialized<Constraint>() == null )
        return;

      m_nativeConstraint = Constraint.Native;
      // The constraint forces in the constraint DOFs are always
      // available, this enables the constraint to compute the
      // resulting forces and torques applied to each body in the
      // constraint. This feature is disabled by default.
      m_nativeConstraint.setEnableComputeForces( true );

      m_stepForceData = new List<ForceData>();
    }

    private void OnEnable()
    {
      // Collect the data as soon as the solver has solved the system.
      Simulation.Instance.StepCallbacks.SimulationPost += OnSimulationPost;
    }

    private void OnDisable()
    {
      // Remove us from the callback when this component has been disabled.
      Simulation.Instance.StepCallbacks.SimulationPost -= OnSimulationPost;
    }

    /// <summary>
    /// Called after the dynamics solvers are done and the constraint forces
    /// has been computed by the constraint.
    /// </summary>
    private void OnSimulationPost()
    {
      if ( m_nativeConstraint == null )
        return;

      // Collect forces and torques applied to the two bodies this current step.
      agx.Vec3 rb1Force = new agx.Vec3(), rb1Torque = new agx.Vec3();
      agx.Vec3 rb2Force = new agx.Vec3(), rb2Torque = new agx.Vec3();
      m_nativeConstraint.getLastForce( 0, ref rb1Force, ref rb1Torque );
      m_nativeConstraint.getLastForce( 1, ref rb2Force, ref rb2Torque );

      m_stepForceData.Add( new ForceData()
      {
        Time             = (float)Simulation.Instance.Native.getTimeStamp(),
        RigidBody1Force  = rb1Force.ToHandedVector3(),
        RigidBody1Torque = rb1Torque.ToHandedVector3(),
        RigidBody2Force  = rb2Force.ToHandedVector3(),
        RigidBody2Torque = rb2Torque.ToHandedVector3()
      } );
    }

    private agx.Constraint m_nativeConstraint = null;
    private List<ForceData> m_stepForceData = null;
  }
}

3.3.2. Contact Callbacks

There are two contact events signed OnContact and OnForce. OnContact callbacks are invoked after collision detection and before the step callback SimulationPre - see figure in Simulation Callbacks. At this point it’s possible to monitor and/or manipulate the contact data before passed to the dynamics solvers, solving the contact. The contact data is accessible in all SimulationPre callbacks but will be cleared after that.

The other, OnForce callbacks, are invoked before SimulationPost when the contact force data is available. The contact and force data is available in OnForce and in all SimulationPost callbacks but will be cleared after that.

An important note, in general, only a subset of all contacts in the simulation will have its representation as Contact Data. Only the contact data that the registered callbacks expects are generated where each callback acts as a filter for which contact data that should be generated. This means that it’s not possible to only have a Step Callbacks SimulationPre and/or SimulationPost and expect the data to be available. You’ll have to register a callback as well for the contact you’re interested in. For details, see Listening to Contact Data.

3.3.2.1. Contact Data

The AGXUnity.ContactData struct is representing the agxCollide.GeometryContact class in AGX Dynamics. The data is an array of Contact Point Data, enabled, two interacting components and the two geometries (agxCollide.Geometry) matched to respective component.

// Complete struct and API documentation in AGXUnity/AGXUnity/ContactData.cs.
public struct ContactData
{
  public ScriptComponent Component1;
  public ScriptComponent Component2;
  public bool Enabled;
  public RefArraySegment<ContactPointData> Points;
  public agxCollide.Geometry Geometry1 { get; private set; }
  public agxCollide.Geometry Geometry2 { get; private set; }

  ...
}

Property

Description

Component1

First interacting component, null if not found.

Component2

Second interacting component, null if not found.

Enabled

True if this contact is enabled and will be evaluated by the solver, false if disabled and about to be removed.

Points

Contact points in this contact.

Geometry1

The agxCollide.Geometry in contact - representing Component1.

Geometry2

The agxCollide.Geometry in contact - representing Component2.

Note

Accessing Geometry1 and/or Geometry2 will create garbage and it’s only safe to access the geometries during the lifetime of the contact data, i.e., OnContact and SimulationPre, or OnForce and SimulationPost. If a copy of the contact data is stored, make sure to invalidate the geometries before: contactData.InvalidateGeometries().

3.3.2.2. Contact Point Data

The AGXUnity.ContactPointData struct is representing the agxCollide.ContactPoint class in AGX Dynamics. Data such as contact normal and tangents (friction directions), contact depth, surface velocity and enabled state are available for each contact point. Manipulation of this data, affecting the system, is only possible in OnContact callbacks.

Contact forces in Contact Point Force Data are only available in OnForce and SimulationPost, i.e., the force data is null in OnContact and SimulationPre. If you’re unsure, verify point.HasForce == true and/or contactData.HasContactPointForceData == true.

// Complete struct and API documentation in AGXUnity/AGXUnity/ContactData.cs.
public struct ContactPointData
{
  public Vector3 Position;
  public Vector3 Normal;
  public Vector3 PrimaryTangent;
  public Vector3 SecondaryTangent;
  public Vector3 SurfaceVelocity;
  public float Depth;
  public bool Enabled;
  public bool HasForce;
  public ContactPointForceData Force;

  ...
}

Property

Description

Position

Position of this contact point in world coordinate frame.

Normal

Contact normal of this contact point in world coordinate frame.

PrimaryTangent

Primary tangent (friction) direction of this contact point in world coordinate frame.

SecondaryTangent

Secondary tangent (friction) direction of this contact point in world coordinate frame.

SurfaceVelocity

Target surface velocity (think conveyor belt) of this contact point in world coordinate frame.

Depth

Penetration depth of this contact point.

Enabled

True if this contact point is enabled and will be evaluated by the solver, false if disabled.

HasForce

True if this contact point has force data, i.e., this contact point has been solved.

Force

Contact point forces available in OnForce and/or SimulationPost. This data is a Nullable<T> but a default instance (all zeros) is returned if this property is accessed when the data isn’t available. The force data is always accessed by value and can only be modified locally.

3.3.2.3. Contact Point Force Data

The AGXUnity.ContactPointForceData struct is representing the force data in the agxCollide.ContactPoint class in AGX Dynamics. This data is Nullable<T> in the Contact Point Data even though a default instance is returned if its property Force is accessed without having read the force data from an agxCollide.ContactPoint. This data is available in OnForce and SimulationPost.

// Complete struct and API documentation in AGXUnity/AGXUnity/ContactData.cs.
public struct ContactPointForceData
{
  public Vector3 Normal;
  public Vector3 PrimaryTangential;
  public Vector3 SecondaryTangential;
  public bool IsImpacting;
  public Vector3 Tangential => PrimaryTangential + SecondaryTangential;
  public Vector3 Total => Normal + Tangential;
}

The field names of ContactPointForceData needs context to make sense. Each field/property could have the postfix Force but will then be redundant given they are accessed from Contact Point Data, i.e., contactPoint.Force.Normal vs. contactPoint.Force.NormalForce. Think field/property postfix Force in the following table:

Property

Description

Normal

Normal force given in world coordinate frame.

PrimaryTangential

Primary tangential (friction) force given in world coordinate frame.

SecondaryTangential

Secondary tangential (friction) force given in world coordinate frame.

IsImpacting

True if the solver solved the contact point as impacting, where restitution of the Contact Material was used, otherwise false.

Tangential

Total tangential (friction) force given in world coordinate frame.

Total

Total (normal + friction) force given in world coordinate frame.

3.3.2.4. Listening to Contact Data

Given all contacts in the simulation, listeners (callbacks in this context) acts as filters matching contacts that should be processed. This is the behavior of AGX Dynamics agxSDK.ContactEventListener with a given agxSDK.ExecuteFilter. I.e., every registered listener only receives callbacks for contacts they are interested in. The filtering is applied for performance and convenience reasons when it’s not effective in any context for each listener to do the filtering. Each implementation of an agxSDK.ExecuteFilter is matching two instances of an agxCollide.Geometry present in a given agxCollide.GeometryContact, and if any implementation of the agxSDK.ExecuteFilter matches the pair of geometries its corresponding listener is invoked.

AGX Dynamics for Unity is using an agxSDK.ExecuteFilter which is matching corresponding UUIDs of a geometry to a registered component. Only AGX Dynamics knows what a given agxCollide.Geometry instance belongs to. E.g., a Wire or a Cable dynamically manages many native geometries that doesn’t have a representation in AGX Dynamics for Unity. The filter enables match where a subject geometry corresponds to an UUID of a Wire or Cable.

To register a contact callback/listener:

private bool MyContactCallback( ref AGXUnity.ContactData contactData )
{
  var hasModifications = false;

  ...

  return hasModifications;
}
  • AGXUnity.Simulation.Instance.ContactCallbacks.OnContact( MyContactCallback, ... )

  • AGXUnity.Simulation.Instance.ContactCallbacks.OnForce( MyContactCallback, ... )

  • AGXUnity.Simulation.Instance.ContactCallbacks.OnContactAndForce( MyContactCallback, ... )

  • Custom AGXUnity.ContactListener [Advanced].

where

  • OnContact is listening to contacts after collision detection and before the dynamics solvers. These callbacks may modify the contact data and the data will be propagated to the simulation if/when a callback returns true, see Modifying Contact Data for more information.

  • OnForce is listening to contacts after the dynamics solvers has solved the contacts. Contact Point Force Data are available in these callbacks. Any modification to the contact data has zero effect in the simulation.

  • OnContactAndForce and the given callback will receive both OnContact and OnForce. contactData.HasContactPointForceData is false while in OnContact and true in OnForce.

and ... is an arbitrary number of AGXUnity.ScriptComponent instances. The behavior of the contact filtering depends on the number of given AGXUnity.ScriptComponent instances, where

  • 0: All contacts in the simulation will be passed to the callback.

  • 1: All contacts interacting with the given component will be passed to the callback.

  • >= 2: All contacts between the given components will be passed to the callback.

E.g.,

namespace Scripts
{
  public class MyContactListener : MonoBehaviour
  {
    public AGXUnity.Wire Wire = null;
    public AGXUnity.Collide.Box Box = null;
    public AGXUnity.RigidBody ARigidBody = null;
    public AGXUnity.RigidBody AnotherRigidBody = null;

    private void Start()
    {
      // 0 components: OnContactAll will receive all contacts in the simulation.
      AGXUnity.Simulation.Instance.ContactCallbacks.OnContact( OnContactAll );

      // 1 component: OnContactBox will receive contacts where our Box is involved.
      AGXUnity.Simulation.Instance.ContactCallbacks.OnContact( OnContactBoxAnything, Box );

      // 2 components: OnContactARigidBodyAnotherRigidBody will receive contacts
      //               between ARigidBody and AnotherRigidBody only.
      AGXUnity.Simulation.Instance.ContactCallbacks.OnContact( OnContactARigidBodyAnotherRigidBody,
                                                               ARigidBody,
                                                               AnotherRigidBody );

      // 4 components: OnContactInternalObjects will receive contacts between
      //               our Wire, Box, ARigidBody and/or AnotherRigidBody.
      AGXUnity.Simulation.Instance.ContactCallbacks.OnContact( OnContactInternalObjects,
                                                               Wire,
                                                               Box,
                                                               ARigidBody,
                                                               AnotherRigidBody );
    }

    private bool OnContactAll( ref AGXUnity.ContactData contactData )
    {
      return false;
    }

    private bool OnContactBoxAnything( ref AGXUnity.ContactData contactData )
    {
      return false;
    }

    private bool OnContactARigidBodyAnotherRigidBody( ref AGXUnity.ContactData contactData )
    {
      return false;
    }

    private bool OnContactInternalObjects( ref AGXUnity.ContactData contactData )
    {
      return false;
    }
  }
}

Note

When listening to a Rigid Body, the component of its interacting shape will be in the contact data Component1 or Component2 instead of the registered Rigid Body.

3.3.2.4.1. Modifying Contact Data

All available data supports modification with the exception of the components and geometries involved. The data will be synchronized with its native representation when an OnContact callback returns true.

The following example is calculating and modifying the target surface velocity of each contact point in a contact. The surface velocity is calculated such that the subject will rotate about its up axis given that the shape is symmetric relative its center axis, e.g., Box.

using UnityEngine;
using AGXUnity;

namespace Scripts
{
  public class MyContactListener : MonoBehaviour
  {
    public float SurfaceSpeed = 1.0f;

    private void Start()
    {
      var rb = GetComponent<RigidBody>();
      if ( rb == null ) {
        Debug.LogWarning( "MyContactListener: Expecting a RigidBody component.", this );
        return;
      }

      Debug.Log( "Modifying surface velocity of " + rb.name + "." );
      Simulation.Instance.ContactCallbacks.OnContact( OnContact, rb );
    }

    private bool OnContact( ref ContactData data )
    {
      foreach ( ref var point in data.Points ) {
        var centerToPoint = ( point.Position - Vector3.Scale( data.Component1.transform.position,
                                                              new Vector3( 1, 0, 1 ) ) ).normalized;
        point.SurfaceVelocity = SurfaceSpeed * Vector3.Cross( centerToPoint, Vector3.up );
      }

      return true;
    }
  }
}

Note that ref var point is used in the foreach loop over the contact points. Without ref var there, it’s an error to write data to point (error CS1654: Cannot modify members of ‘point’ because it is a ‘foreach iteration variable’).

3.3.2.4.2. Custom AGXUnity.ContactListener [Advanced]

Registering callbacks results in an AGXUnity.ContactListener being created and added to the AGXUnity.ContactEventHandler of a simulation. It’s possible to subclass AGXUnity.ContactListener and change behavior of the contacts being matched and collected.

The example below is configuring a listener that listens to objects in contact with Sphere instances in a scene. Overridden methods are Initialize and Remove. As described in the comments, Initialize is called every step from a PreStepForward registered by the AGXUnity.ContactEventListener. It’s called all the time because a listener may wait for certain objects or implement other types of runtime functionality. What’s important is that the filter is up to date before the collision detection is performed.

The agxSDK.ExecuteFilter of the default implementation of AGXUnity.ContactListener is an agxSDK.UuidHashCollisionFilter. The filter may be of any type of the agxSDK.ExecuteFilter classes in AGX Dynamics but agxSDK.UuidHashCollisionFilter is the preferred due to the connection of AGX Dynamics for Unity components with a Native property and its UUID with the usage of uuidToComponentMap[ agxSDK.UuidHashCollisionFilter.findCorrespondingUuid( geometry ) ] when determining Component1 and Component2 in the matched contact data. When a filter different from agxSDK.UuidHashCollisionFilter is used, make sure to override both Initialize and Remove because the default implementations of these methods assumes the filter is of type agxSDK.UuidHashCollisionFilter.

Note that the main behavior change of this example listener is how the native filter is consistent in its matches given an arbitrarily number of subjects (spheres in this example). The mode of the agxSDK.UuidHashCollisionFilter is always set to MATH_OR. This means that only, at least one of the interacting geometries has to be a match in our UUID set. The modes of the filter are MATCH_ALL, MATCH_OR and MATCH_AND, and it corresponds to the operator used to result in a match. Given two geometries in an agxCollide.GeometryContact, the matching implementation basically is (C++):

bool match( const agxCollide::Geometry* geometry1, const agxCollide::Geometry* geometry 2 ) const
{
   return getMode() == MATCH_ALL ||
          ( getMode() == MATCH_OR && ( match( geometry1 ) || match( geometry2 ) ) ) ||
          ( getMode() == MATCH_AND && ( match( geometry1 ) && match( geometry2 ) ) );
}

bool match( const agxCollide::Geometry* geometry ) const
{
  const auto rbUuid = geometry->getRigidBody() != nullptr ?
                        geometry->getRigidBody()->getUuid().hash() :
                        0u;
  return uuidSet.contains( findCorrespondingUuid( geometry ) ) ||
         uuidSet.contains( rbUuid );
}

Make sure to read all the comments in the following example, all comments are specific to this example.

using UnityEngine;
using AGXUnity;

namespace Scripts
{
  public class SphereContactListener : ContactListener
  {
    public SphereContactListener()
    {
      // IMPORTANT: Callback has to be assigned before a listener
      //            is added to the contact event handler.
      Callback = OnContact;
    }

    /// <summary>
    /// Called each step from a PreStepForward callback in the
    /// contact event handler this listener belongs to.
    /// </summary>
    /// <param name="handler">Contact event handler this listener belongs to.</param>
    public override void Initialize( ContactEventHandler handler )
    {
      // Already initialized.
      if ( Filter != null )
        return;

      var filter = new agxSDK.UuidHashCollisionFilter();

      var spheres = Object.FindObjectsOfType<AGXUnity.Collide.Sphere>();
      Debug.Log( "Scripts.SphereContactListener: Found " +
                 spheres.Length +
                 " spheres to listen to." );

      // Adding the UUIDs of the spheres to our filter.
      foreach ( var sphere in spheres ) {
        var uuid = handler.GetUuid( sphere );
        if ( uuid == 0u ) {
          Debug.LogWarning( "Unable to find UUID for sphere " + sphere.name  + "." );
          continue;
        }

        filter.add( uuid );
      }

      // Save the spheres in our Components array to keep track of the number
      // spheres left when objects are destroyed (see our Remove).
      Components = spheres;

      // MATCH_ALL: Any pair of geometries is a match (listen to all contacts).
      // MATCH_OR:  At least one of the corresponding UUID must match our UUID set.
      // MATCH_AND: Both corresponding UUIDs must match our UUID set.
      //
      // We want to match if at least one of the colliding objects is a sphere.
      filter.setMode( agxSDK.UuidHashCollisionFilter.Mode.MATCH_OR );

      // Add our filter given an activation mask for the agxSDK::ContactEventListener
      // being created as a result of this call.
      handler.GeometryContactHandler.Native.add( Filter, (int)m_activationMask );

      // Our filter instance is used to fetch matched contact indices from
      // the AGXUnity.GeometryContactHandler when the callbacks are performed.
      Filter = filter;
    }

    /// <summary>
    /// Called from our ContactEventHandler when a component is about to be
    /// destroyed. If it is a sphere we remove it from our array of components.
    /// When all our spheres has been destroyed we return true, meaning this
    /// listener will also be removed from the ContactEventHandler.
    /// </summary>
    /// <param name="uuid">Native UUID being removed.</param>
    /// <param name="component">Component about to be destroyed.</param>
    /// <param name="notifyOnRemove">
    /// True if we should print a message about our decision to be removed.
    /// </param>
    /// <returns>True if this listener should be remove, otherwise false.</returns>
    public override bool Remove( uint uuid, ScriptComponent component, bool notifyOnRemove = true )
    {
      var sphere = component as AGXUnity.Collide.Sphere;
      if ( sphere == null )
        return false;

      var filter = GetFilter<agxSDK.UuidHashCollisionFilter>();
      if ( filter != null ) {
        if ( !filter.contains( uuid ) )
          return false;

        filter.remove( uuid );
      }

      Components = System.Array.FindAll( Components, sphereComponent => sphereComponent != component );

      var removeUs = filter == null || Components.Length == 0;
      if ( removeUs && notifyOnRemove ) {
        Debug.Log( "Scripts.SphereListener: Component " +
                   component.name +
                   " was our last sphere - removing us from listening to spheres." );
      }

      return removeUs;
    }

    private bool OnContact( ref ContactData data )
    {
      Debug.Assert( data.Component1 is AGXUnity.Collide.Sphere ||
                    data.Component2 is AGXUnity.Collide.Sphere );
      return false;
    }
  }
}

To add our custom contact lister do, e.g., AGXUnity.Simulation.Instance.ContactCallbacks.Add( new SphereContactListener() ).

3.3.2.5. Performance

The filters and contact event listeners collecting matching contacts are implemented in AGX Dynamics, which in general is optimal performance. I.e., there is no interop between C++ and .NET when the contact event listeners are executed. The managed Contact Data is generated and updated without any garbage being waisted. Still, listening to contacts isn’t free of CPU time, and it’s important that the contact callbacks are configured such that the enabled filters and generated contacts both are as low as possible.

Consider a simulation with many contacts. Generating matched Contact Data takes approximately 1 to 1.5 milliseconds per 1000 contact points. Filtering the contacts in AGX Dynamics is an O(N*M) operation, where N is the total number of geometry contacts in the simulation and M the number of filters. Considering the total number of geometry contacts as a constant (not easy to affect), it’s desired that M << N, i.e., having a low number of filters listening to many contacts is better for performance than having many filters matching less contacts. The filtering performance explodes when N >> 1 and N ~= M.

3.4. Custom Target Tool [Editor]

On top of having custom Inspector Editors it’s possible to implement extended functionality for a class using tools. A Custom Target Tool is a top level tool for a given target type that’s active when the given type is rendered in the Inspector with the AGX Dynamics for Unity custom Inspector Editor. Any active Custom Target Tool is the root of a tree of other tools related to functionality of the target type. E.g., AGXUnityEditor.Tools.ConstraintTool is a Custom Target Tool for type AGXUnity.Constraint with several other tools (in breadth and depth), e.g., selecting parent, edge, point, direction etc., of a constraint.

Extending our MyRigidBody script in Native data synchronization with a custom tool, simply add a class inheriting from AGXUnityEditor.Tools.CustomTargetTool with the AGXUnityEditor.CustomTool( typeof( MyRigidBody ) ) attribute:

#if UNITY_EDITOR
namespace Scripts.Editor
{
  [UnityEditor.CustomEditor( typeof( MyRigidBody ) )]
  public class MyRigidBodyEditor : AGXUnityEditor.InspectorEditor
  {
  }

  [AGXUnityEditor.CustomTool( typeof( MyRigidBody ) )]
  public class MyRigidBodyTool : AGXUnityEditor.Tools.CustomTargetTool
  {
    /// <summary>
    /// Construction with the current selection of our target type.
    /// </summary>
    /// <param name="targets">Selected targets of type MyRigidBody.</param>
    public MyRigidBodyTool( Object[] targets )
      : base( targets )
    {
    }
  }
}
#endif

Our tool is instantiated when our target type, MyRigidBody, is selected or when the number of selected targets is changed. Overriding OnPreTargetMembersGUI, called before members are being rendered in the Inspector, adding a tool button to select parent in the Scene View. OnPostTargetMembersGUI is also overridden to show where we are in the Inspector when that method is called and OnSceneViewGUI to render our rigid body sphere.

#if UNITY_EDITOR
namespace Scripts.Editor
{
  [UnityEditor.CustomEditor( typeof( MyRigidBody ) )]
  public class MyRigidBodyEditor : AGXUnityEditor.InspectorEditor
  {
  }

  [AGXUnityEditor.CustomTool( typeof( MyRigidBody ) )]
  public class MyRigidBodyTool : AGXUnityEditor.Tools.CustomTargetTool
  {
    /// <summary>
    /// Construction with the current selection of our target type.
    /// </summary>
    /// <param name="targets">Selected targets of type MyRigidBody.</param>
    public MyRigidBodyTool( Object[] targets )
      : base( targets )
    {
    }

    /// <summary>
    /// True if our select parent tool is active, otherwise false.
    /// </summary>
    public bool SelectParentActive
    {
      get { return GetChild<AGXUnityEditor.Tools.SelectGameObjectTool>() != null; }
      set
      {
        // Activate the tool and set parent of our targets
        // to the given game object if selected.
        if ( value && !SelectParentActive ) {
          var selectParentTool = new AGXUnityEditor.Tools.SelectGameObjectTool();
          selectParentTool.OnSelect += parent =>
          {
            using ( new AGXUnityEditor.Utils.UndoCollapseBlock( "Set parent" ) )
              foreach ( var target in GetTargets<MyRigidBody>() )
                UnityEditor.Undo.SetTransformParent( target.transform,
                                                     parent.transform,
                                                     "New parent for " + target.name );
          };
          AddChild( selectParentTool );
        }
        // Deactivate the tool.
        if ( !value && SelectParentActive )
          RemoveChild( GetChild<AGXUnityEditor.Tools.SelectGameObjectTool>() );
      }
    }

    public override void OnPreTargetMembersGUI()
    {
      // Called before the members of MyRigidBody is rendered in
      // the Inspector.

      var buttonIcon = AGXUnityEditor.ToolIcon.SelectParent;
      var buttonTooltip = "Select parent of our target(s) MyRigidBody component(s) transform.";
      // Button is pressed, toggle active state.
      System.Action onButtonPressed = () => SelectParentActive = !SelectParentActive;
      var buttonData = AGXUnityEditor.InspectorGUI.ToolButtonData.Create( buttonIcon,
                                                                          SelectParentActive,
                                                                          buttonTooltip,
                                                                          onButtonPressed );
      AGXUnityEditor.InspectorGUI.ToolButtons( buttonData );

      if ( SelectParentActive )
        AGXUnityEditor.InspectorGUI.ToolDescription( "Select parent by clicking an " +
                                                     "object in Scene View." );

      UnityEditor.EditorGUILayout.TextField( "This is <b>before</b> members of " +
                                             Targets[ 0 ].GetType().FullName +
                                             " are being rendered.",
                                             AGXUnityEditor.InspectorEditor.Skin.TextField );

      AGXUnityEditor.InspectorGUI.Separator( height: 1, space: 4 );
    }

    public override void OnPostTargetMembersGUI()
    {
      // Called after the members of MyRigidBody has been rendered
      // in the Inspector.

      AGXUnityEditor.InspectorGUI.Separator( height: 1, space: 4 );

      UnityEditor.EditorGUILayout.TextField( "This is <b>after</b> members of " +
                                             Targets[ 0 ].GetType().FullName +
                                             " are being rendered.",
                                             AGXUnityEditor.InspectorEditor.Skin.TextField );
    }

    public override void OnSceneViewGUI( UnityEditor.SceneView sceneView )
    {
      // Scene view update, from here we can render windows and/or
      // objects in the scene view.

      foreach ( var target in GetTargets<MyRigidBody>() )
        DebugRender( target.transform.position, 0.5f, Color.red );
    }
  }
}
#endif

The Inspector before the MyRigidBodyTool is activated:

_images/scripting_myrigidbody_agxunity_inspector.png

The Inspector after the MyRigidBodyTool is activated:

_images/scripting_myrigidbody_tool_inspector.png