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:
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;
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;
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.
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());
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.
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); } } }