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:
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:
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:
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.
3.3.1. Step Callbacks¶
Step callbacks are callbacks made during the simulation step. In order:
PreStepForward: First callback from within
AGXUnity.Simulation
. This callback can, e.g., be used to change initial conditions for the current step.PreSynchronizeTransforms: Callback from within
AGXUnity.Simulation
. This callback is used to write current transforms to native instances.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.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.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.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.PostSynchronizeTransforms: Callback from within
AGXUnity.Simulation
. This callback is used to read transforms from the native simulation, writing them to the corresponding Unity transforms.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 three contact events signed OnContact
, OnForce
and OnSeparation
. The OnContact
and OnSeparation
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.
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.
OnSeparation
callbacks, describing component pairs previously in contact (last step), are invoked before any OnContact
callbacks and has a different
signature since there is no contact data. See Separation Data for more information on separations.
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, |
Component2 |
Second interacting component, |
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 |
Geometry2 |
The |
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
, OnSeparation
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 |
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. Separation Data¶
The AGXUnity.SeparationData
struct in OnSeparation
represents two previously overlapping components that no longer are in contact.
// Complete struct and API documentation in AGXUnity/AGXUnity/ContactData.cs.
public struct SeparationData
{
public ScriptComponent Component1;
public ScriptComponent Component1;
public agxCollide.Geometry Geometry1 { get; private set; }
public agxCollide.Geometry Geometry2 { get; private set; }
}
Property |
Description |
---|---|
Component1 |
First separating component, |
Component2 |
Second separating component, |
Geometry1 |
The separating native |
Geometry2 |
The separating native |
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
, OnSeparation
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.5. 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;
}
private void MySeparationCallback( AGXUnity.SeparationData separationData )
{
...
}
AGXUnity.Simulation.Instance.ContactCallbacks.OnContact( MyContactCallback, ... )
AGXUnity.Simulation.Instance.ContactCallbacks.OnForce( MyContactCallback, ... )
AGXUnity.Simulation.Instance.ContactCallbacks.OnContactAndForce( MyContactCallback, ... )
AGXUnity.Simulation.Instance.ContactCallbacks.OnSeparation( MySeparationCallback, ... )
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.OnSeparation is listening to contacts from the previous step that was removed after the collision detection of the current step. Unlike the contact and force callbacks it is not possible to modify the contacts themselves in the OnSeparation callback.
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.5.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.5.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.
ContactCallback = OnContact;
SeparationCallback = OnSeparation;
// Default activation mask is IMPACT | CONTACT so SEPARATION has to be added or the separation callback will be ignored
m_activationMask = agxSDK.ContactEventListener.ActivationMask.IMPACT |
agxSDK.ContactEventListener.ActivationMask.CONTACT |
agxSDK.ContactEventListener.ActivationMask.SEPARATION;
}
/// <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;
}
private void OnSeparation( SeparationData data )
{
Debug.Assert( data.Component1 is AGXUnity.Collide.Sphere ||
data.Component2 is AGXUnity.Collide.Sphere );
}
}
}
To add our custom contact lister do, e.g., AGXUnity.Simulation.Instance.ContactCallbacks.Add( new SphereContactListener() )
.
3.3.2.6. 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. Runtime Objects¶
A common problem that arises when scripting is the need to create and manage multiple gameobjects
in a hierarchical way. In AGXUnity these objects are managed by a single instance object which is added to the scene
called the RuntimeObjects
singleton. This class allows the user to create gameobjects under a single
root object which makes it easy to localize runtime objects in the scene. This is used in various places throughout
the AGXUnity plugin. A couple of examples include GameObject rendering of Cables and
Terrain particles, as well as debug rendering by the
Debug Render Manager. When runtime objects are created, they are added to a child object under the
RuntimeObject singleton and are given a unique name.
3.5. 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:
The Inspector after the MyRigidBodyTool
is activated: