Scripting Actors

Obi organizes particles and constraints into ObiActors. Examples of actors include: a piece of cloth, a rope, or a fluid emitter. Most of the time actors can be created in the editor, turned into a prefab and instantiated at runtime. However there are situations where creating actors entirely trough code is needed (e.g. procedural level generation). In this section we will learn how to do it.

These are the steps needed to create an actor programmatically:

  • Create a new GameObject and add the required components to it.
  • Setup references between components/GameObjects. Make any adjustments required to the actor's properties.
  • Call actor.GeneratePhysicRepresentationForMesh() -and wait for it to finish, since it is a coroutine-
  • Perform any adjustments to the actor's particles/constraints.

Depending on which kind of actor (cloth, rope, or emitter) you're creating, the GameObject will need certain components, and the setup will be slightly different.

ObiCloth

  • ObiSolver (you can place it in any GameObject, and share the same solver across multiple actors)
  • ObiCloth / ObiTearableCloth

You need to set the cloth's Solver and SharedTopology. The easiest way to create a topology at runtime is to simply create a copy of an existing one using ScriptableObject.CreateInstance. Assuming the topology is also created at runtime (as opposed to loaded as an asset) let's see an example:

GameObject clothObject = new GameObject("cloth",typeof(ObiSolver),
						typeof(ObiCloth));

// get references to all components:
ObiCloth cloth  = clothObject.GetComponent();
ObiSolver solver = clothObject.GetComponent();

// generate the topology:
ObiMeshTopology topology = ScriptableObject.CreateInstance(sourceTopology);
topology.InputMesh = mesh;
topology.Generate();

// set the cloth topology and solver:
cloth.Solver = solver;
cloth.SharedTopology = topology;
			

ObiRope

  • ObiSolver (you can place it in any GameObject, and share the same solver across multiple actors)
  • ObiCatmullRomCurve / ObiBezierCurve, depending on which kind of curve you want to use to control the rope's shape.
  • ObiRope
  • ObiRopeCursor (optional, add it in case you need to change the rope's length at runtime).

You need to set the rope's Solver, ropePath and section. Let's see an example:

GameObject ropeObject = new GameObject("rope",  typeof(ObiSolver),
						typeof(ObiRope),
						typeof(ObiCatmullRomCurve),
						typeof(ObiRopeCursor));

// get references to all components:
ObiSolver solver 	= ropeObject.GetComponent<ObiSolver>();
ObiRope rope 		= ropeObject.GetComponent<ObiRope>();
ObiCatmullRomCurve path = ropeObject.GetComponent<ObiCatmullRomCurve>();
ObiRopeCursor cursor 	= ropeObject.GetComponent<ObiRopeCursor>();

// set up component references
rope.Solver = solver;
rope.ropePath = path;
rope.section = Resources.Load<ObiRopeSection>("DefaultRopeSection");
cursor.rope = rope;
			

ObiEmitter

  • ObiSolver (you can place it in any GameObject, and share the same solver across multiple actors)
  • ObiEmitter
  • any ObiEmitterShape subclass (depending on your needs)
  • ObiParticleRenderer

ObiEmitters are really easy to create:

GameObject emitterObject = new GameObject("emitter",  typeof(ObiSolver),
							typeof(ObiEmitter),
							typeof(ObiEmitterShapeDisk),
							typeof(ObiParticleRenderer));

// get references to all components:
ObiSolver solver 			= emitterObject.GetComponent<ObiSolver>();
ObiEmitter emitter 			= emitterObject.GetComponent<ObiEmitter>();
ObiEmitterShapeDisk shape 		= emitterObject.GetComponent<ObiEmitterShapeDisk>();
ObiParticleRenderer particleRenderer	= emitterObject.GetComponent<ObiParticleRenderer>();

// set up component references
emitter.Solver = solver;
shape.emitter = emitter;
			

Also you will often want to set the emitter's material to an existing one:

emitter.EmitterMaterial = yourMaterial;
			

Or instantiate an existing one (remember, use ScriptableObject.CreateInstance()) and then set its parameters.

ObiSoftbody

  • ObiSolver (you can place it in any GameObject, and share the same solver across multiple actors)
  • ObiSoftbody
  • SkinnedMeshRenderer
  • ObiSoftbodySkinner (add once the SkinnedMeshRenderer has a sharedMesh)
GameObject softbodyObject = new GameObject("softbody", typeof(ObiSolver),
                                                           typeof(ObiSoftbody),
                                                           typeof(SkinnedMeshRenderer));

// get references to all components:
ObiSoftbody softbody = softbodyObject.GetComponent<ObiSoftbody>();
ObiSolver solver = softbodyObject.GetComponent<ObiSolver>();
SkinnedMeshRenderer skrenderer = softbodyObject.GetComponent<SkinnedMeshRenderer>();

// set up component references
softbody.Solver = solver;
softbody.inputMesh = skrenderer.sharedMesh = yourMesh;

// generate particles and constraints here (see section below)

// bind the mesh to the particles:
ObiSoftbodySkinner skinner = softbodyObject.AddComponent<ObiSoftbodySkinner>();
yield return StartCoroutine(skinner.BindSkin());
			

Generating particles and constraints

Once you've set your components up, all that's left is to tell Obi to generate the particle-based representation of the actor, and tell the ObiSolver to include it in the simulation. This is always done exactly the same way, regardless of what kind of actor you're creating:

yield return actor.StartCoroutine(actor.GeneratePhysicRepresentationForMesh());
rope.AddToSolver(null);
			

Now, if you're familiar with coroutines, this will make sense. Generating the particles/constraints is a potentially slow operation, and calling it in a completely syncrhonous way would stall your game for a while. Instead, GeneratePhysicRepresentationForMesh is a coroutine. The yield instruction will allow your game to keep running until GeneratePhysicRepresentationForMesh finishes. Once it is done, it will continue executing where it left: actor.AddToSolver(null);

At this point, particles and constraints have been created for your actor, it has been included in the solver and will start simulating right away. You can now change particle properties, add/remove constraints, etc.

Putting it all together

For reference, here's a complete example: create a pendulum at runtime from scratch using ObiRope:

using System;
using System.Collections;
using UnityEngine;
using Obi;

public class RuntimeRopeGenerator
{
	private ObiRope rope;
	private ObiRopeCursor cursor;
	private ObiSolver solver;


	/// Creates a straight rope anchored to a transform at the top.
	/// Transform may or may not move around and may or may not have a rigidbody.
	/// When you call this the rope will appear in the scene and
	/// immediately interact with gravity and objects with ObiColliders.
	/// Called from anywhere (main thread only)
	public IEnumerator MakeRope(Transform anchoredTo, Vector3 attachmentOffset, float ropeLength)
	{
		// create a new GameObject with the required components: a solver, a rope, and a curve.
		// we also throw a cursor in to be able to change its length.
		GameObject ropeObject = new GameObject("rope",typeof(ObiSolver),
								typeof(ObiRope),
								typeof(ObiCatmullRomCurve),
								typeof (ObiRopeCursor));

		// get references to all components:
		rope 					= ropeObject.GetComponent<ObiRope>();
		cursor 					= ropeObject.GetComponent<ObiRopeCursor>();
		solver 					= ropeObject.GetComponent<ObiSolver>();
		ObiCatmullRomCurve path = ropeObject.GetComponent<ObiCatmullRomCurve>();

		// set up component references:
		rope.Solver = solver;
		rope.ropePath = path;
		rope.section = Resources.Load<ObiRopeSection>("DefaultRopeSection");
		cursor.rope = rope;

		// set path control points
		// (duplicate end points, to set curvature as required by CatmullRom splines):
		path.controlPoints.Clear();
		path.controlPoints.Add(Vector3.zero);
		path.controlPoints.Add(Vector3.zero);
		path.controlPoints.Add(Vector3.down*ropeLength);
		path.controlPoints.Add(Vector3.down*ropeLength);

		// parent the rope to the anchor transform:
		rope.transform.SetParent(anchoredTo,false);
		rope.transform.localPosition = attachmentOffset;

		// generate particles/constraints and add them to the solver:
		yield return rope.StartCoroutine(rope.GeneratePhysicRepresentationForMesh());
		rope.AddToSolver(null);

		// fix first particle in place
		rope.invMasses[0] = 0;
		Oni.SetParticleInverseMasses(solver.OniSolver,new float[]{0},1,rope.particleIndices[0]);
	}


	/// MakeRope and AddPendulum may NOT be called on the same frame.
	/// You must wait for the MakeRope coroutine to finish first.
	/// Just adds a pendulum to the rope on the un-anchored end.
	public void AddPendulum(ObiCollider pendulum, Vector3 attachmentOffset)
	{
		// simply add a new pin constraint
		rope.PinConstraints.RemoveFromSolver(null);
		ObiPinConstraintBatch batch = (ObiPinConstraintBatch)rope.PinConstraints.GetFirstBatch();
		batch.AddConstraint(rope.UsedParticles-1, pendulum, attachmentOffset, 1);
		rope.PinConstraints.AddToSolver(null);
	}


	/// RemovePendulum and AddPendulum may be called on the same frame.
	public void RemovePendulum()
	{
		// simply remove all pin constraints
		rope.PinConstraints.RemoveFromSolver(null);
		rope.PinConstraints.GetFirstBatch().Clear();
		rope.PinConstraints.AddToSolver(null);
	}


	/// Like extending or retracting a winch.
	public void ChangeRopeLength(float changeAmount)
	{
		// the cursor will automatically add/remove/modify constraints and particles as needed.
		cursor.ChangeLength(rope.RestLength + changeAmount);
	}
}
			

Here's how to use the above class:

using System;
using System.Collections;
using UnityEngine;
using Obi;

public class RuntimeRopeGeneratorUse : MonoBehaviour
{
	public ObiCollider pendulum;
	RuntimeRopeGenerator rg;

	public IEnumerator Start()
	{
		rg = new RuntimeRopeGenerator();

		// Create a rope:
		yield return rg.MakeRope(transform,Vector3.zero,1);

		// Add a pendulum
		// You should adjust the attachment point depending on your particularobject.
		// In this case, the object is assumed to be a 1x1x1 cube, so the attachment point is
		// in the middle of its upper face (expressed in local space).
		rg.AddPendulum(pendulum,Vector3.up*0.5f);
	}

	public void Update(){

		if (Input.GetKey(KeyCode.W)){
			rg.ChangeRopeLength(- Time.deltaTime);
		}

		if (Input.GetKey(KeyCode.S)){
			rg.ChangeRopeLength(  Time.deltaTime);
		}

	}
}