Latest  | Search | Go
Edit this page   |   Attach file 

  Home | Tutorials | Technical Reference | Runtime | API Documentation | NovodeXPhysics  


Using NovodeX Physics in Script

Author:Jeremy Stieglitz


Concept of Using Rigid Body Physics In Script

Reality Engine allows usage of 3rd-party physics packages with a minimum of fuss. By enabling the Actor's PHYS_RIGIDBODYDYANMICS PhysicsFlags, the Engine will not attempt to internally update the physics of the Actor. Instead, it then falls to you the programmer to initialize the custom physics representation upon construction, update the transformation of the Actor in an arbritrary manner, and then free the physics representation upon destruction. In this tutorial, we'll examine how to use the varied NovodeX API in C# Script to do exactly that.

Creating a basic Actor with NovodeX Physics

To begin with, let's see how we can take a simple MActor and apply NovodeX physics to it. We'll begin with a simple box with rigid body physics. Let's look at the code to initialize this "RigidBox":

   /// -----------------------------------------------
   /// PRECACHING & STATIC DATA VALUES
   /// -----------------------------------------------
   /// 
   public static string ClassName = "RigidBox";
   private static bool HasCached = false;
   public static void Precache() { HasCached = MPrecacher.Precache(ClassName, HasCached); }
   /// <summary>
   /// MEDIA
   /// </summary>
   static private MModel StaticModel = MPrecacher.PrecacheModel(ClassName, "BBox.xml");
   /// -----------------------------------------------
   /// -----------------------------------------------
   /// 

   MNxActor myNxActor;
   public RigidBox(MWorld world) : base(world)
   {
      Precache();

      MyModel = new MModel();
      StaticModel.CreateNewInstance(MyModel);

      PhysicsFlags = PHYSICS_FLAGS.PHYS_RIGIDBODYDYANMICS;
      CollisionFlags = COLLISION_FLAGS.CF_MESH;
      mass = 1;
   }

The ctor simply loads the Preached model, creates a shallow instance of it, and then sets the appropriate flags: PhysicsFlag PHYS_RIGIDBODYDYANMICS since we'll be handling the physics through NovodeX in Script, and CollisionFlag CF_MESH since we'll want mesh collisions onto the RigidBox from other non-RigidBody Actors (since Mesh collisions will give us the best approximation of the oriented bounding boxes that NovodeX itself uses).

Next, we have to actually create the NxActor that will handle the physics of this RigidBox, and then every Tick we'll copy the transformation of that NxActor into the transformation of the RigidBox Actor. We'll actually initialize the NxActor in Tick() as well (but only once), so that any starting transformation given to the RigidBox can initially be copied into the NxActor during its creation. Let's look at how the Tick both creates the NxActor (once) and then copies its transformation into the RigidBox MActor (every frame):

   public override void Tick()
   {
      // only create the NxActor if we haven't already created it
      if (myNxActor == null)
      {
         // init the NxBody description, give it our Actor mass
         MNxBodyDesc BodyDesc = new MNxBodyDesc();
         BodyDesc.Mass = mass;

         // init the NxShape description (box), giving it the 
         // identity AABB size of this Actor's Model
         MNxBoxShapeDesc boxDesc = new MNxBoxShapeDesc();
         MyModel.SetTransform(MHelpers.IdentityMatrix);
         boxDesc.Dimensions = MyModel.GetWorldBBoxMax();

         // init our NxActor description with the NxBody and NxShape
         MNxActorDesc ActorDesc = new MNxActorDesc();
         ActorDesc.Body = BodyDesc;
         ActorDesc.AddShapeDesc(boxDesc);

         // create our NxActor
         myNxActor = MNxPhysics.CreateNxActor(ActorDesc, this);

         // copy the Actor's current transformation into the 
         // initial transformation of our NxActor
         myNxActor.Position = Location;
         myNxActor.Orientation = Rotation;
      }

      // if this Actor isn't selected in Reality Builder, 
      // then copy the NxActor's transformation into it
      if (!IsSelected)
      {
         Location = myNxActor.Position;
         Rotation = myNxActor.Orientation;
      }
      // otherwise copy its transformation into the NxActor, 
      // since when we're dragging this Actor 
      // around in Reality Builder, we want to have control over it
      else 
      {
         myNxActor.Position = Location;
         myNxActor.Orientation = Rotation;
      }

      // pass Tick along to the base class
      base.Tick();
   }

As you can see above, our order of operations in the Tick is:

  • Setup and create the NxActor if we haven't already created it (so that will happen once, upon first Tick). Note that we give set identity transformation on the Model before we get its World bounding box, this will ensure bounding box extent values with respect to the origin -- that's what we want the dimensions of the NxBoxShape to be.
    • We also give the newly-created NxActor the current transformation of the MActor, which will be its starting transformation upon the first Tick.
  • Then, every Tick, we either copy the transformation of the dynamic NxActor into the transformation of the MActor (if the MActor is not selected in Reality Builder), or we copy the transformation of the MActor into the NxActor (since if it's selected in the editor, we'll want to have control over it).
  • Finally, we call the base Tick to ensure that lighting, model transforms, and spatial partitions are properly updated.

Let's look at how to free the NxActor upon destruction of the owner MActor:

   protected override void  DisposeResources()
   {
      MNxPhysics.DestroyNxActor(myNxActor);
      myNxActor= null;
      base.DisposeResources();
   }

Simple stuff, we just call the MNxPhysics.DestroyNxActor function and nullify our reference. Now you know how to set Engine flags for arbritrary rigid body physics, initialize and update from an NxActor, and free the NxActor upon destruction of the Parent Actor. With these basic concepts behind us, let's examine some more advanced aspects of using NovodeX within Reality Script.

Creating NovodeX Materials

NovodeX Materials allow you to customize how your NxActor behaves during collisions with other surfaces; there are many variables within the NxMaterial that you can alter to achieve different effects. Creating an NxMaterial is simple, as follows:

   static ushort WheelMaterialIndex;
   public static void Precache()
   {
      if (!HasCached)
      {
         MNxMaterial WheelMaterial = new MNxMaterial();
         WheelMaterial.Restitution = 0.25f;
         WheelMaterial.DynamicFriction = 0.75f;
         WheelMaterial.StaticFriction = 0.90f;
         WheelMaterialIndex = MNxPhysics.AddMaterial(WheelMaterial);
      }
      HasCached = MPrecacher.Precache(ClassName, HasCached);
   }

Though it's not required, it is good practice to store the returned value from MNxPhysics.AddMaterial in a "static ushort" that you can then use for all instances of the Actor that you intend to have the same surface properties. This will prevent you from creating excessive Material ID's per instance. You can set the MaterialIndex equal to said "static ushort" in the ShapeDesc you use when creating your NxActor, like so:

...
         // init the NxShape description (box), giving it the 
         // identity AABB size of this Actor's Model
         MNxBoxShapeDesc boxDesc = new MNxBoxShapeDesc();
         MyModel.SetTransform(MHelpers.IdentityMatrix);
         boxDesc.Dimensions = MyModel.GetWorldBBoxMax();
         // set our box shape's MaterialIndex to our custom WheelMaterialIndex
         // otherwise it would use the default Material at Index 0
         boxDesc.MaterialIndex = WheelMaterialIndex;
...

Using NovodeX Joints

Joints allow you to tie rigid body NxActors together, restricting their relative movement and rotation in any number of degrees of freedom. Creating joints is simply a matter of initializing the appropriate Joint description with the appropriate NxActors that you want to connect. The one concern is whether you want to keep the joint as a separate MActor class itself and/or connect two separate MActors' NxActors, or whether you want to contain the entire system inside one MActor and manually draw the system's Models as you see fit.

Let's look at the latter, simpler scenario, in which we have one MActor that contains the entire system and we manually draw the appropriate Models to represent the individual rigid body NxActors within the system. Here are the class var declarations for this "JointedBoxes" class:

   /// -----------------------------------------------
   /// PRECACHING & STATIC DATA VALUES
   /// -----------------------------------------------
   /// 
   public static string ClassName = "JointedBoxes";
   private static bool HasCached = false;
   public static void Precache() { HasCached = MPrecacher.Precache(ClassName, HasCached); }
   /// <summary>
   /// MEDIA
   /// </summary>
   static private MModel StaticModel = MPrecacher.PrecacheModel(ClassName, "BBox.xml");
   /// -----------------------------------------------
   /// -----------------------------------------------
   /// 

   MNxActor myNxActor;
   MNxActor mySecondNxActor;
   MNxJoint myNxJoint;
   MModel   mySecondModel;

Note that we're now storing a reference to a second NxActor (in addition to the original), an NxJoint, and a second Model for our extra rigid box, which we'll manually draw in MActor.OnRender(). The ctor of this class:

   public JointedBoxes(MWorld world)
      : base(world)
   {
      Precache();

      MyModel = new MModel();
      StaticModel.CreateNewInstance(MyModel);
      mySecondModel = new MModel();
      StaticModel.CreateNewInstance(mySecondModel);

      PhysicsFlags = PHYSICS_FLAGS.PHYS_RIGIDBODYDYANMICS;
      CollisionFlags = COLLISION_FLAGS.CF_MESH;
      mass = 1;
   }

Nothing too unique here, just constructing and loading the second shallow Model. The heavy lifting of constructing our second NxActor and the joint will be in our physics initialization, in the first Tick():

public override void Tick()
{
   // only create the NxActor if we haven't already created it
   if (myNxActor == null)
   {
      // init the NxBody description, give it our Actor mass
      MNxBodyDesc BodyDesc = new MNxBodyDesc();
      BodyDesc.Mass = mass;

      // init the NxShape description (box), giving it the 
      // identity AABB size of this Actor's Model
      MNxBoxShapeDesc boxDesc = new MNxBoxShapeDesc();
      MyModel.SetTransform(MHelpers.IdentityMatrix);
      boxDesc.Dimensions = MyModel.GetWorldBBoxMax();

      // init our NxActor description with the NxBody and NxShape
      MNxActorDesc ActorDesc = new MNxActorDesc();
      ActorDesc.Body = BodyDesc;
      ActorDesc.AddShapeDesc(boxDesc);

      // create our NxActor
      myNxActor = MNxPhysics.CreateNxActor(ActorDesc, this);

      // copy the Actor's current transformation into the 
      // initial transformation of our NxActor
      myNxActor.Position = Location;
      myNxActor.Orientation = Rotation;

      // offset our second NxActor to the (relative) Right of our first
      MVector SecondLocation = Location + Rotation.GetRight()*2.0f;
      mySecondNxActor = MNxPhysics.CreateNxActor(ActorDesc, this);
      mySecondNxActor.Position = SecondLocation;
      mySecondNxActor.Orientation = Rotation;

      //Create a description for the new Cylindrical Joint
      MNxRevoluteJointDesc jointDesc = new MNxRevoluteJointDesc();
      // bind it to both NxActors
      jointDesc.Actor1 = myNxActor;
      jointDesc.Actor2 = mySecondNxActor;

      // put it in the middle of the two NxActors
      jointDesc.GlobalAnchor = Location + Rotation.GetRight();
      jointDesc.GlobalAxis = Rotation.GetRight();

      //Create the Joint
      myNxJoint = MNxPhysics.CreateNxJoint(jointDesc);
   }

   // if this Actor isn't selected in Reality Builder, 
   // then copy the NxActor's transformation into it
   if (!IsSelected)
   {
      Location = myNxActor.Position;
      Rotation = myNxActor.Orientation;
   }
   // otherwise copy its transformation into the NxActor, 
   // since when we're dragging this Actor 
   // around in Reality Builder, we want to have control over it
   else
   {
      myNxActor.Position = Location;
      myNxActor.Orientation = Rotation;
   }

   // pass Tick along to the base class
   base.Tick();
}

The new lines of code, in the part of the Tick() that constructs the NxActors, simply creates a second NxActor (with the same settings description as the first), and offsets its position 2m to the right of the first NxActor. Then, it creates a Revolute (Hinge) joint that is bound to both NxActors, places its anchor in between the two NxActors, and sets the revolution axis to the (relative) Right axis so that the two NxActors will with respect to each other like the wheels on a car.

Notice that when we update the Actor's Location in the bottom of the Tick(), we'll only concern ourselves with the first myNxActor. This is because we don't need to worry about the transformation of mySecondNxActor here, we'll just grab that transformation when we want to render the mySecondModel. Which we'll do right here, in MActor.OnRender():

public override void OnRender(MCamera camera)
{
   mySecondModel.SetTransform(mySecondNxActor.Orientation, mySecondNxActor.Position);
   mySecondModel.Draw(MyWorld,this);
   base.OnRender(camera);
}

As you can see, there we simply copy the transformation of mySecondNxActor into our second Model, and draw the second Model. The first Model we created (MyModel) will automatically be drawn for this Actor because it is this Actor's MyModel. The last thing that have to concern ourselves with is freeing of the joint upon Actor destruction:

protected override void DisposeResources()
{
   MNxPhysics.DestroyNxJoint(myNxJoint);
   myNxJoint = null;
   MNxPhysics.DestroyNxActor(myNxActor);
   myNxActor = null;
   base.DisposeResources();
}

Simple enough. Here you've got a Hinge-style joint that will "roll" these two NxActors with respect to each other. There are many other types of joints with varying degrees of freedom that you can use in the same manner (by setting up their Descs and then calling MNxPhysics.CreateJoint), including (with their Degrees of Freedom):

NovodeX Collision Notification

Now that you've got your NxActors, you may want feedback when they collide, so that you can apply damage or custom game logic to this event. Getting notification of collisions is quite simple, simply add the following code when you create your NxActor:

...
// create our NxActor
myNxActor = MNxPhysics.CreateNxActor(ActorDesc, this);
// set collision notification Event
myNxActor.OnCollision += new CollisionNotify(myNxActor_OnCollision);
...

Then, define your collision Event elsewhere in the class:

void myNxActor_OnCollision(int pairIndex, MNxActor other, MVector normalForce)
{
   // optionally create a CollisionInfo struct 
   // and pass the collision to the overridable 
   // MActor.RigidBodyContact function
   MCollisionInfo info = new MCollisionInfo();
   info.normal = normalForce;
   if(other != null)
      info.touched = other.Parent;
   RigidBodyContact(info);
}

As you can see, the above code fills out a standard MCollisionInfo struct and passes it to the overridable MActor.RigidBodyContact function. These steps are optional, they're just a decent standard way of handling collisions. Note how the code also checks whether the other NxActor collided against is null, this is just a safety precaution: even unmanaged NxActors will be automatically wrapped as MNxActors here, so you can for instance get information about the collisions onto Prefabs' unmanaged triangle meshes. However, don't count on a non-null value for Parent, since MNxActor's Parent will be actually null unless they're MNxActors that were created by passing a Parent MActor reference to MNxPhysics.CreateNxActor.

That's all there is to basic Collision Notification! But let's examine how to get more information about collisions, such as contact points, by reading data from the Contact Streams.

Getting Collision information from NovodeX Contact Streams

Contact Streams allow you to get detailed information about a collision, during its Collision Notification Event. You can find out what specific Shapes were involved, and most importantly what the exact set of collision points was. This is very useful for detailed contact behavior, such as contact-point accurate wheel forces on a vehicle. The first thing to keep in mind about reading from Contact Streams is that it can only be within your Collision Notification Event. Also, you must completely iterate through the Contact Stream, do not bail out early. Those caveats aside, reading from Contact Streams is quite straightforward. Here's how it's done:

myNxActor.OnCollision += new CollisionNotify(myNxActor_OnCollision);
...
...
public override void rigidBody_OnCollision(int pairIndex, MNxActor other, MVector normalForce)
{
// we want to start accessing the contact stream
// this must be paired with a ContactStopIteration() some time later!
MNxPhysics.ContactStartIteration();

//user could call getNumPairs() here
while(MNxPhysics.ContactGoNextPair())
   {
   //user could also call getNumPatches() here
   MNxShape s = MNxPhysics.ContactGetShape(pairIndex);
   while(MNxPhysics.ContactGoNextPatch())
      {
      //user could also call getPatchNormal() and getNumPoints() here
      while(MNxPhysics.ContactGoNextPoint())
         {
         //user could also getSeparation() here
         MVector contactPoint = MNxPhysics.ContactGetPoint();

         //assuming front wheel drive we need to apply a force at the wheels.
         if (s.ShapeType == MNxShapeType.NX_SHAPE_CAPSULE)      
            //assuming only the wheels of the car are capsules, otherwise we need more checks.
            //this branch can't be pulled out of loops because we have to do a full 
            // iteration through the stream
            {
            CarWheelContact cwc;
            cwc.WheelIndex = s.UserData;
            cwc.ContactPoint = contactPoint;
            CarWheelContacts.Add(cwc);
            }
         }
      }      
   }

// we're done with the contactstream
MNxPhysics.ContactStopIteration();
}

As you can see in the code above, the first thing to do when you want to access a Contact Stream (again, only during the collision notification event), is to call MNxPhysics.ContactStartIteration(). This must be paired with a MNxPhysics.ContactStopIteration(), which itself must only be called after the entire stream has been iterated. After calling MNxPhysics.ContactStartIteration(), you can iterate through all the shapes in the stream (those involved in the contact) with. As common sense would dictate, there's always a pair of shapes in a Contact, indices 0 and 1. The pairIndex of the NxShape owned by specific NxActor triggering this Collision Notification is passed to you in the Event. You can also call MNxShape.getActor() if you desire to get the NxActor to which the shape is bound (by which you could also get the MNxActor.Parent).

Regardless of the NxShapes you want to check, you do need to iterate through all stream's Contact Points. In this example, the contact points are stored in a custom structure in an array, which are used later to apply wheel forces at the points at which the wheels actually touch the ground. You could just as easily spawn sparks at these points when a car scrapes a wall, apply damage decals, play sound effects in 3d space, or whatever you wish. Bear in mind however that you should not apply forces or otherwise alter rigid body physics within the StartIteration()/StopIteration() block. That's why this example stores the points into an array for application of forces later.

Representing non-RigidBody Actors in the Simulation

If you want your NovodeX Actors to collide with non-NovodeX Actors, you have several options. Prefabs -- the World geometry -- are automatically represented as static triangle meshes. With Non-Prefabs, the decision is left to you. By default, non-Prefab non-NovodeX Actors have no representation in the NovodeX simulation. However, by enabling the CollisionFlag CF_INCLUDE_RIGIDBODY_SIMULATION, the Engine will automatically create a representation for you. For CF_MESH Actors, it will automatically create a trianglemesh, and for non-CF_MESH Actors, it will create a kinematic box of the bounding box size of the Actor's MyModel (or the CollisionBox size if the Actor has no MyModel). This kinematic representation will allow NxActors to collide with it (and receive appropriate Collision Notification), but can not itself be pushed or affected by forces -- it will automatically transform along with the owner Actor.

So the CF_INCLUDE_RIGIDBODY_SIMULATION flag very useful for, say, to allow rigid bodies to collide onto a non-RigidBody Player's bounding box. Or for a CF_MESH custom Actor to allow rigid bodies to collide onto it accurately per-triangle. These kinematic representations will even push regular rigid bodies out of the way should you transform them through such bodies (useful for hinge doors opening, lifts, and other mechanisms). With the CF_INCLUDE_RIGIDBODY_SIMULATION, the Engine will automatically create the kinematic represenation and free it upon Actor destruction.

However, if you clear the flag, the Engine will not automatically remove the kinematic representation. To force removal of the kinematic representation before the Actor is destroyed, use the MActor.RemoveFromRigidBodySimulation() function (which will also automatically clear the CF_INCLUDE_RIGIDBODY_SIMULATION flag).

You can also manually create these kinematic reprentations, affording you greater control over their behavior (for instance, disabling collisions onto the kinematic when you don't want them occur, such as when a Player is a corpse). Then it is your responsibility to create, update, and free the NxActors as usual. Activating "kinematic" behavior for any of your NxActors is simply a matter of calling the NxActor.RaiseBodyFlag(MNxBodyFlag.NX_BF_KINEMATIC), and you can control whether collisions occur with NxActor.SetCollisionsEnabled().

To encapsulate this behavior for convenience, we also have the NxKinematicBox helper class (among others).

Creating TriangleMesh Shapes through Script is also quite simple. Use the MNxTriangleMeshShapeDesc and, after constructing it, call MNxTriangleMeshShapeDesc.SetMesh() with the model you wish to generate the TriangleMesh from.

Base Script Helper Classes

A set of classes contained in Scripts\Base\NxPhysics.cs is provided to aid in usage of common single-Shape rigid bodies. They adhere to a simple interface of using a Create() function to initialize the rigid body, and automatically pass collisions to the Parent Actor's RigidBodyContact function. Additionally they include a variety of useful Properties, and clean up their resources via the standard Dispose() method. Here's how the simple NxBox helper can be used to abstract the physics of our "RigidBox":

public class RigidBox : MActor
{
...
NxBox myRigidBody;
...
public override void Tick()
{
   // only create the rigid body if we haven't already created it
   if (myRigidBody == null)
   {
      myRigidBody = new NxBox();
      MyModel.SetTransform(MHelpers.IdentityMatrix);
      myRigidBody.Create(this, MyModel.GetWorldBBoxMax(), mass, 0);
   }

   // if this Actor isn't selected in Reality Builder, 
   // then copy the rigid body's transformation into it
   if (!IsSelected)
   {
      Location = myRigidBody.Location;
      Rotation = myRigidBody.Rotation;
   }
   // otherwise copy its transformation into the rigid body, 
   // since when we're dragging this Actor 
   // around in Reality Builder, we want to have control over it
   else
   {
      myRigidBody.Location = Location;
      myRigidBody.Rotation = Rotation;
   }

   // pass Tick along to the base class
   base.Tick();
}

public override void RigidBodyContact(MCollisionInfo info)
{
   MHelpers.Printf("RigidBox collision!");
}

protected override void DisposeResources()
{
   myRigidBody.Dispose();
   myRigidBody = null;
   base.DisposeResources();
}

Note the differences from our earlier RigidBox code:

  • Create() function takes all our parameters, no need to worry about creating Descs.
  • We don't have to set Position/Orientation after creating the rigid body, because the NxBox helper class does that for us.
  • The NxBox has standard Location/Rotation/Velocity Physics Properties that map to the rigid body.
  • Automatic passing of collisions to RigidBodyContact (we don't have to set up the Collision Notification Event since that's done internally in the NxBox)
  • Standard Dispose() interface rather than direct freeing of NxActor

If the standard helper classes don't suit your needs, you may well want to write your own classes that extend NxBody, which contains the overridable base interface for these helper classes. For more complex systems that can't easily fit within the interface that NxBody presents (such as multi-NxActor systems), you may want to contain your physics code directly within your MActor class (as we did in the multi-NxActor "JointedBoxes"). The choice is up to you,it is completely optional to use these helper classes. However you may find it prudent to use such classes to contain your physics systems, for their Methods and Properties do abstract the rigid body physics one layer from your game code, making changing Physics API's or internal functionality possible without directly affecting your game code.

NovodeXPhysics   Edit | Attach | Ref-By | Printable | Diffs | r1.8 | > | r1.7 | > | r1.6 | More