添加插件

This commit is contained in:
2025-11-10 00:08:26 +08:00
parent 4059c207c0
commit 76f80db694
2814 changed files with 436400 additions and 178 deletions

View File

@@ -0,0 +1,33 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
public class CableCarController : MonoBehaviour
{
public ObiPinhole pinhole;
public float carSpeed = 1;
// Update is called once per frame
void Update()
{
float speed = 0;
if (Input.GetKey(KeyCode.W))
{
speed = carSpeed;
}
if (Input.GetKey(KeyCode.S))
{
speed = -carSpeed;
}
pinhole.motorSpeed = speed;
if (Input.GetKeyDown(KeyCode.Space))
{
pinhole.friction = pinhole.friction > 0.5f ? 0 : 1;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: df6783efff96747d7b949f3538d1cee7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,79 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
public class CharacterControl2D : MonoBehaviour
{
public float floorRaycastDistance = 1.2f;
[Header("Grounded")]
public float acceleration = 80;
public float maxSpeed = 6;
public float damping = 0.005f;
public float jumpPower = 10;
[Header("Airborne")]
public float airAcceleration = 16;
public float airMaxSpeed = 12;
public float extraGravity = -12;
[Header("Auto upright")]
public Vector3 centerOfMass = new Vector3(0, -0.25f, 0);
public float P = 2;
public float D = 0.1f;
private Rigidbody unityRigidbody;
private float axis;
private bool grounded;
private float error;
private float prevError;
public void Awake()
{
unityRigidbody = GetComponent<Rigidbody>();
unityRigidbody.centerOfMass = centerOfMass;
}
private void Update()
{
axis = Input.GetAxisRaw("Horizontal");
grounded = Physics.Raycast(new Ray(transform.position, -Vector3.up), floorRaycastDistance);
if (Input.GetButtonDown("Jump") && grounded)
unityRigidbody.AddForce(Vector3.up * jumpPower, ForceMode.VelocityChange);
}
private void FixedUpdate()
{
var velocity = unityRigidbody.linearVelocity;
prevError = error;
error = Vector3.SignedAngle(unityRigidbody.transform.up, Vector3.up, Vector3.forward);
if (grounded)
{
float accel = axis * acceleration * Time.deltaTime;
if ((velocity.x < maxSpeed && accel > 0) || (velocity.x > -maxSpeed && accel < 0))
velocity.x += accel;
if (Mathf.Approximately(axis, 0))
velocity.x *= Mathf.Pow(damping, Time.deltaTime);
unityRigidbody.AddTorque(new Vector3(0, 0, error * P + (error - prevError) / Time.deltaTime * D));
}
else
{
float accel = axis * airAcceleration * Time.deltaTime;
if ((velocity.x < airMaxSpeed && accel > 0) || (velocity.x > -airMaxSpeed && accel < 0))
velocity.x += accel;
velocity.y += extraGravity * Time.deltaTime;
}
unityRigidbody.linearVelocity = velocity;
}
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 82b0f164ac14247fd8a566109fbbe771
labels:
- ObiRope
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
public class CraneController : MonoBehaviour
{
ObiRopeCursor cursor;
ObiRope rope;
public float speed = 1;
// Use this for initialization
void Start()
{
cursor = GetComponentInChildren<ObiRopeCursor>();
rope = cursor.GetComponent<ObiRope>();
}
// Update is called once per frame
void Update()
{
if (Input.GetKey(KeyCode.W))
{
if (rope.restLength > 6.5f)
cursor.ChangeLength(-speed * Time.deltaTime);
}
if (Input.GetKey(KeyCode.S))
{
cursor.ChangeLength(speed * Time.deltaTime);
}
if (Input.GetKey(KeyCode.A))
{
transform.Rotate(0, Time.deltaTime * 15f, 0);
}
if (Input.GetKey(KeyCode.D))
{
transform.Rotate(0, -Time.deltaTime * 15f, 0);
}
}
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 5551d37f45fd044ff8768019501c20a5
labels:
- ObiRope
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
[RequireComponent(typeof(ObiRope))]
public class CursorController : MonoBehaviour
{
public float minLength = 0.1f;
public float speed = 1;
private ObiRopeCursor cursor;
private ObiRope rope;
void OnEnable()
{
rope = GetComponent<ObiRope>();
cursor = GetComponent<ObiRopeCursor>();
}
void Update()
{
float change = 0;
if (Input.GetKey(KeyCode.W) && cursor != null)
{
change -= speed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.S) && cursor != null)
{
change += speed * Time.deltaTime;
}
if (rope.restLength + change < minLength)
change = minLength - rope.restLength;
cursor.ChangeLength(change);
if (Input.GetKey(KeyCode.A))
{
rope.transform.Translate(Vector3.left * Time.deltaTime, Space.World);
}
if (Input.GetKey(KeyCode.D))
{
rope.transform.Translate(Vector3.right * Time.deltaTime, Space.World);
}
}
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 83c7e82b3aac04e408bc8bfe9027c983
labels:
- ObiRope
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,227 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/**
* Sample component that shows how to use Obi Rope to create a grappling hook for a 2.5D game.
* 95% of the code is the grappling hook logic (user input, scene raycasting, launching, attaching the hook, etc) and parameter setup,
* to show how to use Obi completely at runtime. This might not be practical for real-world scenarios,
* but illustrates how to do it.
*
* Note that the choice of using actual rope simulation for grapple dynamics is debatable. Usually
* a simple spring works better both in terms of performance and controllability.
*
* If complex interaction is required with the scene, a purely geometry-based approach (ala Worms ninja rope) can
* be the right choice under certain circumstances.
*/
namespace Obi.Samples
{
public class ExtendableGrapplingHook : MonoBehaviour
{
public ObiSolver solver;
public ObiCollider character;
public Material material;
public ObiRopeSection section;
[Range(0, 1)]
public float hookResolution = 0.5f;
public float hookExtendRetractSpeed = 2;
public float hookShootSpeed = 30;
public int particlePoolSize = 100;
private ObiRope rope;
private ObiRopeBlueprint blueprint;
private ObiRopeExtrudedRenderer ropeRenderer;
private ObiRopeCursor cursor;
private RaycastHit hookAttachment;
void Awake()
{
// Create both the rope and the solver:
rope = gameObject.AddComponent<ObiRope>();
ropeRenderer = gameObject.AddComponent<ObiRopeExtrudedRenderer>();
ropeRenderer.section = section;
ropeRenderer.uvScale = new Vector2(1, 4);
ropeRenderer.normalizeV = false;
ropeRenderer.uvAnchor = 1;
ropeRenderer.material = material;
// Setup a blueprint for the rope:
blueprint = ScriptableObject.CreateInstance<ObiRopeBlueprint>();
blueprint.resolution = 0.5f;
blueprint.pooledParticles = particlePoolSize;
// Tweak rope parameters:
rope.maxBending = 0.02f;
// Add a cursor to be able to change rope length:
cursor = rope.gameObject.AddComponent<ObiRopeCursor>();
cursor.cursorMu = 0;
cursor.direction = true;
}
private void OnDestroy()
{
DestroyImmediate(blueprint);
}
/**
* Raycast against the scene to see if we can attach the hook to something.
*/
private void LaunchHook()
{
// Get the mouse position in the scene, in the same XY plane as this object:
Vector3 mouse = Input.mousePosition;
mouse.z = transform.position.z - Camera.main.transform.position.z;
Vector3 mouseInScene = Camera.main.ScreenToWorldPoint(mouse);
// Get a ray from the character to the mouse:
Ray ray = new Ray(transform.position, mouseInScene - transform.position);
// Raycast to see what we hit:
if (Physics.Raycast(ray, out hookAttachment))
{
// We actually hit something, so attach the hook!
StartCoroutine(AttachHook());
}
}
private void LayParticlesInStraightLine(Vector3 origin, Vector3 direction)
{
// placing all particles in a straight line, respecting rope length
float length = 0;
for (int i = 0; i < rope.elements.Count; ++i)
{
int p1 = rope.elements[i].particle1;
int p2 = rope.elements[i].particle2;
solver.prevPositions[p1] = solver.positions[p1] = origin + direction * length;
length += rope.elements[i].restLength;
solver.prevPositions[p2] = solver.positions[p2] = origin + direction * length;
}
}
private IEnumerator AttachHook()
{
yield return null;
// Clear pin constraints:
var pinConstraints = rope.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;
pinConstraints.Clear();
Vector3 localHit = rope.transform.InverseTransformPoint(hookAttachment.point);
// Procedurally generate the rope path (just a short segment, as we will extend it over time):
int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);
blueprint.path.Clear();
blueprint.path.AddControlPoint(Vector3.zero, Vector3.zero, Vector3.zero, Vector3.up, 0.1f, 0.1f, 1, filter, Color.white, "Hook start");
blueprint.path.AddControlPoint(localHit.normalized * 0.5f, Vector3.zero, Vector3.zero, Vector3.up, 0.1f, 0.1f, 1, filter, Color.white, "Hook end");
blueprint.path.FlushEvents();
// Generate the particle representation of the rope (wait until it has finished):
yield return blueprint.Generate();
// Set the blueprint (this adds particles/constraints to the solver and starts simulating them).
rope.ropeBlueprint = blueprint;
rope.GetComponent<ObiRopeExtrudedRenderer>().enabled = true;
// wait for the solver to load the rope, after the next physics step:
yield return new WaitForFixedUpdate();
yield return null;
// set masses to zero, as we're going to override positions while we extend the rope:
for (int i = 0; i < rope.activeParticleCount; ++i)
solver.invMasses[rope.solverIndices[i]] = 0;
// while the last particle hasn't reached the hit, extend the rope:
Vector3 origin;
Vector3 direction;
while (true)
{
// calculate rope origin in solver space:
origin = solver.transform.InverseTransformPoint(rope.transform.position);
// update direction and distance to hook point:
direction = solver.transform.InverseTransformPoint(hookAttachment.point) - origin;
float distance = direction.magnitude;
direction.Normalize();
LayParticlesInStraightLine(origin, direction);
// increase length:
float distanceLeft = distance - cursor.ChangeLength(hookShootSpeed * Time.deltaTime);
// if we have exceeded the desired length, correct it and break the loop:
if (distanceLeft < 0)
{
cursor.ChangeLength(distanceLeft);
break;
}
// wait for next frame:
yield return null;
}
// wait for the last length change to take effect, and ensure the rope is straight:
yield return new WaitForFixedUpdate();
yield return null;
LayParticlesInStraightLine(origin, direction);
// restore masses so that the simulation takes over now that the rope is in place:
for (int i = 0; i < rope.activeParticleCount; ++i)
solver.invMasses[rope.solverIndices[i]] = 10; // 1/0.1 = 10
// Pin both ends of the rope (this enables two-way interaction between character and rope):
var batch = new ObiPinConstraintsBatch();
batch.AddConstraint(rope.elements[0].particle1, character, transform.localPosition, Quaternion.identity, 0, 0);
batch.AddConstraint(rope.elements[rope.elements.Count - 1].particle2, hookAttachment.collider.GetComponent<ObiColliderBase>(),
hookAttachment.collider.transform.InverseTransformPoint(hookAttachment.point), Quaternion.identity, 0, 0);
batch.activeConstraintCount = 2;
pinConstraints.AddBatch(batch);
rope.SetConstraintsDirty(Oni.ConstraintType.Pin);
}
private void DetachHook()
{
// Set the rope blueprint to null (automatically removes the previous blueprint from the solver, if any).
rope.ropeBlueprint = null;
rope.GetComponent<ObiRopeExtrudedRenderer>().enabled = false;
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
if (!rope.isLoaded)
LaunchHook();
else
DetachHook();
}
if (rope.isLoaded)
{
if (Input.GetKey(KeyCode.W))
{
cursor.ChangeLength(-hookExtendRetractSpeed * Time.deltaTime);
}
if (Input.GetKey(KeyCode.S))
{
cursor.ChangeLength(hookExtendRetractSpeed * Time.deltaTime);
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: b77478c97cefb4dec9cc85236f7c5fca
labels:
- ObiRope
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,155 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/**
* Sample component that shows how to use Obi Rope to create a grappling hook for a 2.5D game.
* 95% of the code is the grappling hook logic (user input, scene raycasting, launching, attaching the hook, etc) and parameter setup,
* to show how to use Obi completely at runtime. This might not be practical for real-world scenarios,
* but illustrates how to do it.
*
* Note that the choice of using actual rope simulation for grapple dynamics is debatable. Usually
* a simple spring works better both in terms of performance and controllability.
*
* If complex interaction is required with the scene, a purely geometry-based approach (ala Worms ninja rope) can
* be the right choice under certain circumstances.
*/
namespace Obi.Samples
{
public class GrapplingHook : MonoBehaviour
{
public ObiSolver solver;
public ObiCollider character;
public float hookExtendRetractSpeed = 2;
public Material material;
public ObiRopeSection section;
private ObiRope rope;
private ObiRopeBlueprint blueprint;
private ObiRopeExtrudedRenderer ropeRenderer;
private ObiRopeCursor cursor;
private RaycastHit hookAttachment;
void Awake()
{
// Create both the rope and the solver:
rope = gameObject.AddComponent<ObiRope>();
ropeRenderer = gameObject.AddComponent<ObiRopeExtrudedRenderer>();
ropeRenderer.section = section;
ropeRenderer.uvScale = new Vector2(1, 4);
ropeRenderer.normalizeV = false;
ropeRenderer.uvAnchor = 1;
rope.GetComponent<MeshRenderer>().material = material;
// Setup a blueprint for the rope:
blueprint = ScriptableObject.CreateInstance<ObiRopeBlueprint>();
blueprint.resolution = 0.5f;
// Tweak rope parameters:
rope.maxBending = 0.02f;
// Add a cursor to be able to change rope length:
cursor = rope.gameObject.AddComponent<ObiRopeCursor>();
cursor.cursorMu = 0;
cursor.direction = true;
}
private void OnDestroy()
{
DestroyImmediate(blueprint);
}
/**
* Raycast against the scene to see if we can attach the hook to something.
*/
private void LaunchHook()
{
// Get the mouse position in the scene, in the same XY plane as this object:
Vector3 mouse = Input.mousePosition;
mouse.z = transform.position.z - Camera.main.transform.position.z;
Vector3 mouseInScene = Camera.main.ScreenToWorldPoint(mouse);
// Get a ray from the character to the mouse:
Ray ray = new Ray(transform.position, mouseInScene - transform.position);
// Raycast to see what we hit:
if (Physics.Raycast(ray, out hookAttachment))
{
// We actually hit something, so attach the hook!
StartCoroutine(AttachHook());
}
}
private IEnumerator AttachHook()
{
yield return 0;
Vector3 localHit = rope.transform.InverseTransformPoint(hookAttachment.point);
int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);
// Procedurally generate the rope path (a simple straight line):
blueprint.path.Clear();
blueprint.path.AddControlPoint(Vector3.zero, -localHit.normalized, localHit.normalized, Vector3.up, 0.1f, 0.1f, 1, filter, Color.white, "Hook start");
blueprint.path.AddControlPoint(localHit, -localHit.normalized, localHit.normalized, Vector3.up, 0.1f, 0.1f, 1, filter, Color.white, "Hook end");
blueprint.path.FlushEvents();
// Generate the particle representation of the rope (wait until it has finished):
yield return blueprint.Generate();
// Set the blueprint (this adds particles/constraints to the solver and starts simulating them).
rope.ropeBlueprint = blueprint;
rope.GetComponent<MeshRenderer>().enabled = true;
// Pin both ends of the rope (this enables two-way interaction between character and rope):
var pinConstraints = rope.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;
pinConstraints.Clear();
var batch = new ObiPinConstraintsBatch();
batch.AddConstraint(rope.solverIndices[0], character, transform.localPosition, Quaternion.identity, 0, 0);
batch.AddConstraint(rope.solverIndices[blueprint.activeParticleCount - 1], hookAttachment.collider.GetComponent<ObiColliderBase>(),
hookAttachment.collider.transform.InverseTransformPoint(hookAttachment.point), Quaternion.identity, 0, 0);
batch.activeConstraintCount = 2;
pinConstraints.AddBatch(batch);
rope.SetConstraintsDirty(Oni.ConstraintType.Pin);
}
private void DetachHook()
{
// Set the rope blueprint to null (automatically removes the previous blueprint from the solver, if any).
rope.ropeBlueprint = null;
rope.GetComponent<MeshRenderer>().enabled = false;
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
if (!rope.isLoaded)
LaunchHook();
else
DetachHook();
}
if (rope.isLoaded)
{
if (Input.GetKey(KeyCode.W))
{
cursor.ChangeLength(rope.restLength - hookExtendRetractSpeed * Time.deltaTime);
}
if (Input.GetKey(KeyCode.S))
{
cursor.ChangeLength(rope.restLength + hookExtendRetractSpeed * Time.deltaTime);
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 94f9aa141b5964f9f91dadcc1919618c
labels:
- ObiRope
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
public class HighlightCollidingRopes : MonoBehaviour
{
public void Highlight(ActorActorCollisionDetector.ActorPair pair)
{
if (pair.actorA.TryGetComponent(out ActorBlinker blinkerA)) blinkerA.Blink(pair.particleA);
if (pair.actorB.TryGetComponent(out ActorBlinker blinkerB)) blinkerB.Blink(pair.particleB);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 50979842bbc734acdb11f0f01062fe92
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,84 @@
using UnityEngine;
namespace Obi.Samples
{
[RequireComponent(typeof(ObiRope))]
public class HosePump : MonoBehaviour
{
[Header("Bulge controls")]
public float pumpSpeed = 5;
public float bulgeFrequency = 3;
public float baseThickness = 0.04f;
public float bulgeThickness = 0.06f;
public Color bulgeColor = Color.cyan;
[Header("Flow controls")]
public ParticleSystem waterEmitter;
public float flowSpeedMin = 0.5f;
public float flowSpeedMax = 7;
public float minEmitRate = 100;
public float maxEmitRate = 1000;
private ObiRope rope;
public ObiPathSmoother smoother;
private float time = 0;
void OnEnable()
{
rope = GetComponent<ObiRope>();
smoother = GetComponent<ObiPathSmoother>();
rope.OnSimulationStart += Rope_OnBeginStep;
}
void OnDisable()
{
rope.OnSimulationStart -= Rope_OnBeginStep;
}
private void Rope_OnBeginStep(ObiActor actor, float stepTime, float substepTime)
{
time += stepTime * pumpSpeed;
float distance = 0;
float sine = 0;
// iterate over all particles, changing their radius and color based on a sine wave:
// (note this would not support resizable / cuttable ropes, to add support for that use rope.elements instead)
for (int i = 0; i < rope.solverIndices.count; ++i)
{
int solverIndex = rope.solverIndices[i];
if (i > 0)
{
int previousIndex = rope.solverIndices[i - 1];
distance += Vector3.Distance(rope.solver.positions[solverIndex], rope.solver.positions[previousIndex]);
}
sine = Mathf.Max(0, Mathf.Sin(distance * bulgeFrequency - time));
rope.solver.principalRadii[solverIndex] = Vector3.one * Mathf.Lerp(baseThickness, bulgeThickness, sine);
rope.solver.colors[solverIndex] = Color.Lerp(Color.white, bulgeColor, sine);
}
// change particle emission rate/speed based on sine wave at the last particle:
if (waterEmitter != null)
{
var main = waterEmitter.main;
main.startSpeed = Mathf.Lerp(flowSpeedMin, flowSpeedMax, sine);
var emission = waterEmitter.emission;
emission.rateOverTime = Mathf.Lerp(minEmitRate, maxEmitRate, sine);
}
}
public void LateUpdate()
{
if (smoother != null && waterEmitter != null)
{
ObiPathFrame section = smoother.GetSectionAt(1);
waterEmitter.transform.position = rope.solver.transform.TransformPoint(section.position);
waterEmitter.transform.rotation = rope.solver.transform.rotation * (Quaternion.LookRotation(section.tangent, section.binormal));
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: d71cdcf242c9b44fb874af7b7d8fd2b9
timeCreated: 1518879675
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
using UnityEngine;
namespace Obi.Samples
{
public class PinholeRatchet : MonoBehaviour
{
public ObiPinhole pinhole;
public bool direction = false;
public float teethSeparation = 0.1f;
public float distanceToNextTooth { get; private set; }
void Update()
{
if (pinhole == null || pinhole.rope == null)
return;
float restLength = (pinhole.rope as ObiRopeBase).restLength;
float normalizedTeethDistance = Mathf.Max(0.001f, teethSeparation / restLength);
var range = pinhole.range;
if (direction)
{
distanceToNextTooth = (range.y - pinhole.position) * restLength;
while (distanceToNextTooth > teethSeparation)
{
range.y -= normalizedTeethDistance;
distanceToNextTooth -= teethSeparation;
}
}
else
{
distanceToNextTooth = (pinhole.position - range.x) * restLength;
while (distanceToNextTooth > teethSeparation)
{
range.x += normalizedTeethDistance;
distanceToNextTooth -= teethSeparation;
}
}
pinhole.range = range;
}
public void OnDisable()
{
if (pinhole != null)
pinhole.range = new Vector2(0, 1);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: de82c5da5889a437bbca59463285c6a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using UnityEngine;
namespace Obi.Samples
{
public class RatchetController : MonoBehaviour
{
public PinholeRatchet ratchet;
public Transform ratchetVisualizer;
public float minAngle = 0;
public float maxAngle = 25;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
ratchet.enabled = !ratchet.enabled;
float angle = ratchet.enabled ? Mathf.LerpAngle(minAngle, maxAngle, ratchet.distanceToNextTooth / ratchet.teethSeparation) : maxAngle;
ratchetVisualizer.rotation = Quaternion.AngleAxis(angle, Vector3.forward);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89f7ec50c6e36440593469d980ba7830
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
[RequireComponent(typeof(LineRenderer))]
public class RenderLineBetweenTransforms : MonoBehaviour
{
public Transform transformA;
public Transform transformB;
LineRenderer line;
void Awake()
{
line = GetComponent<LineRenderer>();
}
void Update()
{
if (transformA != null && transformB != null)
line.SetPositions(new Vector3[] { transformA.position, transformB.position });
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 411d9eba4bb0042bf9302873985bc6e0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
using UnityEngine;
namespace Obi.Samples
{
public class RopeBetweenTwoPoints : MonoBehaviour
{
public Transform start;
public Transform end;
public ObiSolver solver;
public bool fixedParticleCount = true;
public int numParticles = 5;
void Start()
{
// Generate rope synchronously:
Generate();
}
void Generate()
{
if (start != null && end != null)
{
// Adjust our transform:
transform.position = (start.position + end.position) / 2;
transform.rotation = Quaternion.FromToRotation(Vector3.right, end.position - start.position);
// Calculate control point positions and tangent vector:
Vector3 startPositionLS = transform.InverseTransformPoint(start.position);
Vector3 endPositionLS = transform.InverseTransformPoint(end.position);
Vector3 tangentLS = (endPositionLS - startPositionLS).normalized;
// Create the blueprint:
var blueprint = ScriptableObject.CreateInstance<ObiRopeBlueprint>();
// Build the rope path:
int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);
blueprint.path.AddControlPoint(startPositionLS, -tangentLS, tangentLS, Vector3.up, 0.1f, 0.1f, 1, filter, Color.white, "start");
blueprint.path.AddControlPoint(endPositionLS, -tangentLS, tangentLS, Vector3.up, 0.1f, 0.1f, 1, filter, Color.white, "end");
blueprint.path.FlushEvents();
if (fixedParticleCount)
blueprint.resolution = numParticles / (blueprint.path.Length / blueprint.thickness);
// Generate particles/constraints:
blueprint.GenerateImmediate();
// Add rope actor / renderer / attachment components:
var rope = gameObject.AddComponent<ObiRope>();
var ropeRenderer = gameObject.AddComponent<ObiRopeExtrudedRenderer>();
var attachment1 = gameObject.AddComponent<ObiParticleAttachment>();
var attachment2 = gameObject.AddComponent<ObiParticleAttachment>();
// Load the default rope section for rendering:
ropeRenderer.section = Resources.Load<ObiRopeSection>("DefaultRopeSection");
// Set the blueprint:
rope.ropeBlueprint = blueprint;
// Attach both ends:
attachment1.target = start;
attachment2.target = end;
attachment1.particleGroup = blueprint.groups[0];
attachment2.particleGroup = blueprint.groups[1];
// Parent the actor under a solver to start the simulation:
transform.SetParent(solver.transform);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9eaa9e75d3f0a4e119decb19dce797be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,130 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
/**
* Sample component that procedurally generates a net using rigidbodies and ropes.
*/
public class RopeNet : MonoBehaviour
{
public Material material;
public Vector2Int resolution = new Vector2Int(5, 5);
public Vector2 size = new Vector2(0.5f, 0.5f);
public float nodeSize = 0.2f;
void Awake()
{
// create an object containing both the solver and the updater:
GameObject solverObject = new GameObject("solver", typeof(ObiSolver));
ObiSolver solver = solverObject.GetComponent<ObiSolver>();
solver.substeps = 2;
// adjust solver settings:
solver.particleCollisionConstraintParameters.enabled = false;
solver.distanceConstraintParameters.iterations = 8;
solver.pinConstraintParameters.iterations = 4;
solver.parameters.sleepThreshold = 0.001f;
solver.PushSolverParameters();
// create the net (ropes + rigidbodies)
CreateNet(solver);
}
private void CreateNet(ObiSolver solver)
{
ObiCollider[,] nodes = new ObiCollider[resolution.x + 1, resolution.y + 1];
for (int x = 0; x <= resolution.x; ++x)
{
for (int y = 0; y <= resolution.y; ++y)
{
GameObject rb = GameObject.CreatePrimitive(PrimitiveType.Cube);
rb.transform.position = new Vector3(x, y, 0) * size;
rb.transform.localScale = new Vector3(nodeSize, nodeSize, nodeSize);
rb.AddComponent<Rigidbody>();
nodes[x, y] = rb.AddComponent<ObiCollider>();
nodes[x, y].Filter = 1;
}
}
nodes[0, resolution.y].GetComponent<Rigidbody>().isKinematic = true;
nodes[resolution.x, resolution.y].GetComponent<Rigidbody>().isKinematic = true;
for (int x = 0; x <= resolution.x; ++x)
{
for (int y = 0; y <= resolution.y; ++y)
{
Vector3 pos = new Vector3(x, y, 0) * size;
if (x < resolution.x)
{
Vector3 offset = new Vector3(nodeSize * 0.5f, 0, 0);
var rope = CreateRope(pos + offset, pos + new Vector3(size.x, 0, 0) - offset);
rope.transform.parent = solver.transform;
PinRope(rope, nodes[x, y], nodes[x + 1, y]);
}
if (y < resolution.y)
{
Vector3 offset = new Vector3(0, nodeSize * 0.5f, 0);
var rope = CreateRope(pos + offset, pos + new Vector3(0, size.y, 0) - offset);
rope.transform.parent = solver.transform;
PinRope(rope, nodes[x, y], nodes[x, y + 1]);
}
}
}
}
private void PinRope(ObiRope rope, ObiCollider bodyA, ObiCollider bodyB)
{
var A = rope.gameObject.AddComponent<ObiParticleAttachment>();
var B = rope.gameObject.AddComponent<ObiParticleAttachment>();
A.attachmentType = ObiParticleAttachment.AttachmentType.Dynamic;
B.attachmentType = ObiParticleAttachment.AttachmentType.Dynamic;
A.target = bodyA.transform;
B.target = bodyB.transform;
A.particleGroup = rope.ropeBlueprint.groups[0];
B.particleGroup = rope.ropeBlueprint.groups[1];
}
// Creates a rope between two points in world space:
private ObiRope CreateRope(Vector3 pointA, Vector3 pointB)
{
// Create a rope
var ropeObject = new GameObject("solver", typeof(ObiRope), typeof(ObiRopeLineRenderer));
var rope = ropeObject.GetComponent<ObiRope>();
var ropeRenderer = ropeObject.GetComponent<ObiRopeLineRenderer>();
rope.GetComponent<ObiRopeLineRenderer>().material = material;
rope.GetComponent<ObiPathSmoother>().decimation = 0.1f;
ropeRenderer.uvScale = new Vector2(1, 5);
// Setup a blueprint for the rope:
var blueprint = ScriptableObject.CreateInstance<ObiRopeBlueprint>();
blueprint.resolution = 0.15f;
blueprint.thickness = 0.02f;
blueprint.pooledParticles = 0;
// convert both points to the rope's local space:
pointA = rope.transform.InverseTransformPoint(pointA);
pointB = rope.transform.InverseTransformPoint(pointB);
// Procedurally generate the rope path (a simple straight line):
Vector3 direction = (pointB - pointA) * 0.25f;
blueprint.path.Clear();
blueprint.path.AddControlPoint(pointA, -direction, direction, Vector3.up, 0.1f, 0.1f, 1, 1, Color.white, "A");
blueprint.path.AddControlPoint(pointB, -direction, direction, Vector3.up, 0.1f, 0.1f, 1, 1, Color.white, "B");
blueprint.path.FlushEvents();
rope.ropeBlueprint = blueprint;
return rope;
}
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 83e341aa1a9a14825ab84ad04cd5f44c
labels:
- ObiRope
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,150 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
[RequireComponent(typeof(ObiRope))]
public class RopeSweepCut : MonoBehaviour
{
public Camera cam;
ObiRope rope;
LineRenderer lineRenderer;
Vector3 cutStartPosition;
Vector3 cutEndPosition;
bool cut;
private void Awake()
{
rope = GetComponent<ObiRope>();
AddMouseLine();
}
private void OnDestroy()
{
DeleteMouseLine();
}
private void OnEnable()
{
rope.OnSimulationStart += Rope_OnBeginSimulation;
}
private void OnDisable()
{
rope.OnSimulationStart -= Rope_OnBeginSimulation;
}
private void AddMouseLine()
{
GameObject line = new GameObject("Mouse Line");
lineRenderer = line.AddComponent<LineRenderer>();
lineRenderer.startWidth = 0.005f;
lineRenderer.endWidth = 0.005f;
lineRenderer.numCapVertices = 2;
lineRenderer.sharedMaterial = new Material(Shader.Find("Unlit/Color"));
lineRenderer.sharedMaterial.color = Color.cyan;
lineRenderer.enabled = false;
}
private void DeleteMouseLine()
{
if (lineRenderer != null)
Destroy(lineRenderer.gameObject);
}
private void LateUpdate()
{
// do nothing if we don't have a camera to cut from.
if (cam == null) return;
// process user input and cut the rope if necessary.
ProcessInput();
}
private void Rope_OnBeginSimulation(ObiActor actor, float stepTime, float substepTime)
{
if (cut)
{
ScreenSpaceCut(cutStartPosition, cutEndPosition);
cut = false;
}
}
/**
* Very simple mouse-based input. Not ideal for multitouch screens as it only supports one finger, though.
*/
private void ProcessInput()
{
// When the user clicks the mouse, start a line cut:
if (Input.GetMouseButtonDown(0))
{
cutStartPosition = Input.mousePosition;
lineRenderer.SetPosition(0, cam.ScreenToWorldPoint(new Vector3(cutStartPosition.x, cutStartPosition.y, 0.5f)));
lineRenderer.enabled = true;
}
if (lineRenderer.enabled)
lineRenderer.SetPosition(1, cam.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0.5f)));
// When the user lifts the mouse, proceed to cut.
if (Input.GetMouseButtonUp(0))
{
cutEndPosition = Input.mousePosition;
lineRenderer.enabled = false;
cut = true;
}
}
/**
* Cuts the rope using a line segment, expressed in screen-space.
*/
private void ScreenSpaceCut(Vector2 lineStart, Vector2 lineEnd)
{
// keep track of whether the rope was cut or not.
bool ropeCut = false;
// iterate over all elements and test them for intersection with the line:
for (int i = 0; i < rope.elements.Count; ++i)
{
// project the both ends of the element to screen space.
Vector3 screenPos1 = cam.WorldToScreenPoint(rope.solver.positions[rope.elements[i].particle1]);
Vector3 screenPos2 = cam.WorldToScreenPoint(rope.solver.positions[rope.elements[i].particle2]);
// test if there's an intersection:
if (SegmentSegmentIntersection(screenPos1, screenPos2, lineStart, lineEnd, out float r, out float s))
{
ropeCut = true;
rope.Tear(rope.elements[i]);
}
}
// If the rope was cut at any point, rebuilt constraints:
if (ropeCut) rope.RebuildConstraintsFromElements();
}
/**
* line segment 1 is AB = A+r(B-A)
* line segment 2 is CD = C+s(D-C)
* if they intesect, then A+r(B-A) = C+s(D-C), solving for r and s gives the formula below.
* If both r and s are in the 0,1 range, it meant the segments intersect.
*/
private bool SegmentSegmentIntersection(Vector2 A, Vector2 B, Vector2 C, Vector2 D, out float r, out float s)
{
float denom = (B.x - A.x) * (D.y - C.y) - (B.y - A.y) * (D.x - C.x);
float rNum = (A.y - C.y) * (D.x - C.x) - (A.x - C.x) * (D.y - C.y);
float sNum = (A.y - C.y) * (B.x - A.x) - (A.x - C.x) * (B.y - A.y);
if (Mathf.Approximately(rNum, 0) || Mathf.Approximately(denom, 0))
{ r = -1; s = -1; return false; }
r = rNum / denom;
s = sNum / denom;
return (r >= 0 && r <= 1 && s >= 0 && s <= 1);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88cb6f64c607f46b3b0d7a30a99ed18b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
public class RopeTenser : MonoBehaviour
{
public float force = 10;
// Update is called once per frame
void Update()
{
GetComponent<Rigidbody>().AddForce(Vector3.down * force);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6a01ab42f187a43818168cd1cd11d3cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,60 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
[RequireComponent(typeof(ObiRope))]
public class RopeTensionColorizer : MonoBehaviour
{
public float minTension = 0;
public float maxTension = 0.2f;
public Color normalColor = Color.green;
public Color tensionColor = Color.red;
public RopeTenser tenser;
public float tenserThreshold = -5;
public float tenserMax = 0.1f;
private ObiRope rope;
private Material localMaterial;
void Awake()
{
rope = GetComponent<ObiRope>();
var rend = GetComponent<ObiRopeLineRenderer>();
localMaterial = Instantiate(rend.material);
rend.material = localMaterial;
}
private void OnDestroy()
{
Destroy(localMaterial);
}
void Update()
{
if (tenser == null)
return;
// Calculate how much past the threshold the tenser is, clamp the excess to 1
float tenserFactor = Mathf.Min((tenser.transform.position.y - tenserThreshold) / tenserMax, 1);
// Check if the tenser is above the threshold, if so then check rope tension:
if (tenserFactor > 0)
{
// Calculate current tension as ratio between current and rest length, then subtract 1 to center it at zero.
float tension = rope.CalculateLength() / rope.restLength - 1;
// Use tension to interpolate between colors:
float lerpFactor = (tension - minTension) / (maxTension - minTension);
localMaterial.color = Color.Lerp(normalColor, tensionColor, lerpFactor * tenserFactor);
}
else
{
localMaterial.color = normalColor;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 51f5e34b2316b45c6bdf971b375b5f2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,126 @@
using UnityEngine;
namespace Obi.Samples
{
public class SnakeController : MonoBehaviour
{
public Transform headReferenceFrame;
public float headSpeed = 20;
public float upSpeed = 40;
public float slitherSpeed = 10;
private ObiRope rope;
private ObiSolver solver;
private float[] traction;
private Vector3[] surfaceNormal;
private void Start()
{
rope = GetComponent<ObiRope>();
solver = rope.solver;
// subscribe to solver events (to update surface information)
rope.OnSimulationStart += ResetSurfaceInfo;
solver.OnCollision += AnalyzeContacts;
solver.OnParticleCollision += AnalyzeContacts;
}
private void OnDestroy()
{
rope.OnSimulationStart -= ResetSurfaceInfo;
solver.OnCollision -= AnalyzeContacts;
solver.OnParticleCollision -= AnalyzeContacts;
}
private void ResetSurfaceInfo(ObiActor a, float simulatedTime, float substepTime)
{
if (traction == null)
{
traction = new float[rope.activeParticleCount];
surfaceNormal = new Vector3[rope.activeParticleCount];
}
if (Input.GetKey(KeyCode.J))
{
for (int i = 1; i < rope.activeParticleCount; ++i)
{
int solverIndex = rope.solverIndices[i];
int prevSolverIndex = rope.solverIndices[i - 1];
// direction from current particle to previous one, projected on the contact surface:
Vector4 dir = Vector3.ProjectOnPlane(solver.positions[prevSolverIndex] - solver.positions[solverIndex], surfaceNormal[i]).normalized;
// move in that direction:
solver.velocities[solverIndex] += dir * traction[i] / solver.invMasses[solverIndex] * slitherSpeed * simulatedTime;
}
}
int headIndex = rope.solverIndices[0];
if (headReferenceFrame != null)
{
Vector3 direction = Vector3.zero;
// Determine movement direction:
if (Input.GetKey(KeyCode.W))
{
direction += headReferenceFrame.forward * headSpeed;
}
if (Input.GetKey(KeyCode.A))
{
direction += -headReferenceFrame.right * headSpeed;
}
if (Input.GetKey(KeyCode.S))
{
direction += -headReferenceFrame.forward * headSpeed;
}
if (Input.GetKey(KeyCode.D))
{
direction += headReferenceFrame.right * headSpeed;
}
// flatten out the direction so that it's parallel to the ground:
direction.y = 0;
solver.velocities[headIndex] += (Vector4)direction * simulatedTime;
}
if (Input.GetKey(KeyCode.Space))
solver.velocities[headIndex] += (Vector4)Vector3.up * simulatedTime * upSpeed;
// reset surface info:
for (int i = 0; i < traction.Length; ++i)
{
traction[i] = 0;
surfaceNormal[i] = Vector3.zero;
}
}
private void AnalyzeContacts(object sender, ObiNativeContactList e)
{
// iterate trough all contacts:
for (int i = 0; i < e.count; ++i)
{
var contact = e[i];
if (contact.distance < 0.005f)
{
int simplexIndex = solver.simplices[contact.bodyA];
var particleInActor = solver.particleToActor[simplexIndex];
if (particleInActor != null && particleInActor.actor == rope && traction != null)
{
// using 1 here, could calculate a traction value based on the type of terrain, friction, etc.
traction[particleInActor.indexInActor] = 1;
// accumulate surface normal:
surfaceNormal[particleInActor.indexInActor] += (Vector3)contact.normal;
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e5d46d2f0b9f9496f9ed59231aa6982d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
[ExecuteInEditMode]
public class SpiralCurve : MonoBehaviour
{
public float radius = 0.25f;
public float radialStep = 0.8f;
public float heightStep = 0.04f;
public float points = 30;
public float rotationalMass = 1;
public float thickness = 1;
void Awake()
{
Generate();
}
public void Generate()
{
var rod = GetComponent<ObiRopeBase>();
if (rod == null) return;
var blueprint = rod.sourceBlueprint as ObiRopeBlueprintBase;
if (blueprint == null) return;
blueprint.path.Clear();
float ang = 0;
float height = 0;
for (int i = 0; i < points; ++i)
{
Vector3 point = new Vector3(Mathf.Cos(ang) * radius, height, Mathf.Sin(ang) * radius);
// optimal handle length for circle approximation: 4/3 tan(pi/(2n))
Vector3 tangent = new Vector3(-point.z, heightStep, point.x).normalized * (4.0f / 3.0f) * Mathf.Tan(radialStep / 4.0f) * radius;
blueprint.path.AddControlPoint(point, -tangent, tangent, Vector3.up, 1, rotationalMass, thickness, 1, Color.white, "control point " + i);
ang += radialStep;
height += heightStep;
}
blueprint.path.FlushEvents();
}
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: e76488b5e11ef47ba9c0746db5c47115
labels:
- ObiRope
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,88 @@
using System.Collections;
using UnityEngine;
namespace Obi.Samples
{
[RequireComponent(typeof(Rigidbody))]
public class TangledPeg : MonoBehaviour
{
public TangledPegSlot currentSlot;
public Collider floorCollider;
public ObiRope attachedRope;
[Header("Movement")]
public float stiffness = 200;
public float damping = 20;
public float maxAccel = 50;
public float minDistance = 0.05f;
public Rigidbody rb { get; private set; }
public ObiRigidbody orb { get; private set; }
void Awake()
{
rb = GetComponent<Rigidbody>();
orb = GetComponent<ObiRigidbody>();
// Ignore collisions with the floor:
Physics.IgnoreCollision(GetComponent<Collider>(), floorCollider);
// Initialize the peg's current slot, if any:
if (currentSlot != null)
{
currentSlot.currentPeg = this;
transform.position = currentSlot.transform.position;
}
}
public float MoveTowards(Vector3 position)
{
Vector3 vector = position - transform.position;
float distance = Vector3.Magnitude(vector);
// simple damped spring: F = -kx - vu
Vector3 accel = stiffness * vector - damping * rb.linearVelocity;
// clamp spring acceleration:
accel = Vector3.ClampMagnitude(accel, maxAccel);
rb.AddForce(accel, ForceMode.Acceleration);
return distance;
}
public void DockInSlot(TangledPegSlot slot)
{
StopAllCoroutines();
StartCoroutine(MoveTowardsSlot(slot));
}
public void UndockFromCurrentSlot()
{
if (currentSlot != null)
{
currentSlot.currentPeg = null;
rb.isKinematic = false;
}
}
private IEnumerator MoveTowardsSlot(TangledPegSlot slot)
{
float distance = float.MaxValue;
orb.kinematicForParticles = true;
while (distance > minDistance)
{
distance = MoveTowards(slot.transform.position);
yield return 0;
}
currentSlot = slot;
currentSlot.currentPeg = this;
transform.position = currentSlot.transform.position;
rb.isKinematic = true;
orb.kinematicForParticles = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: beb10e70cc06a4b019ecf88348a5f7fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using UnityEngine;
namespace Obi.Samples
{
public class TangledPegSlot : MonoBehaviour
{
public TangledPeg currentPeg;
public Color tintColor;
private Material instance;
private Color normalColor;
public void Awake()
{
instance = GetComponent<Renderer>().material;
normalColor = instance.color;
}
public void Tint()
{
instance.color = tintColor;
}
public void ResetColor()
{
instance.color = normalColor;
}
public void OnDestroy()
{
Destroy(instance);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 866212c246a9c4c5797069099ad745aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,138 @@
using UnityEngine;
using UnityEngine.Events;
namespace Obi.Samples
{
[RequireComponent(typeof(ObiSolver))]
public class TangledRopesGameController : MonoBehaviour
{
public TangledPegSlot[] pegSlots;
public float pegHoverHeight = 1;
public float maxPegDistanceFromSlot = 1.5f;
public int framesWithoutContactsToWin = 30;
public UnityEvent onFinish = new UnityEvent();
private TangledPeg selectedPeg;
private Plane floor = new Plane(Vector3.up, 0);
private int framesSinceLastContact = 0;
void OnEnable()
{
GetComponent<ObiSolver>().OnParticleCollision += Solver_OnParticleCollision;
}
private void OnDisable()
{
GetComponent<ObiSolver>().OnParticleCollision -= Solver_OnParticleCollision;
}
private TangledPegSlot FindCandidateSlot(TangledPeg peg)
{
// Go over all slots, find the closest one to the peg that's closer than
// maxPegDistanceFromSlot:
TangledPegSlot closest = null;
float closestDistance = float.MaxValue;
foreach (TangledPegSlot slot in pegSlots)
{
// reset slot color, to make sure it looks normal if it's not a candidate for our peg.
slot.ResetColor();
// ignore occupied slots:
if (slot.currentPeg != null)
continue;
Vector3 slotOnFloor = floor.ClosestPointOnPlane(slot.transform.position);
Vector3 pegOnFloor = floor.ClosestPointOnPlane(peg.transform.position);
float distance = Vector3.Distance(slotOnFloor, pegOnFloor);
if (distance < closestDistance && distance < maxPegDistanceFromSlot)
{
closest = slot;
closestDistance = distance;
}
}
return closest;
}
private void Update()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// User clicks, cast a ray towards the floor, see if it hits any peg.
if (Input.GetMouseButtonDown(0))
{
if (Physics.Raycast(ray, out RaycastHit hit))
{
// if the ray hit a peg, store it as the selected peg and lift it off from its current slot.
if (hit.transform.TryGetComponent(out TangledPeg peg) && peg.currentSlot != null)
{
selectedPeg = peg;
selectedPeg.UndockFromCurrentSlot();
}
}
}
if (selectedPeg != null)
{
// Make selected peg follow the mouse cursor:
if (floor.Raycast(ray, out float enter))
selectedPeg.MoveTowards(ray.GetPoint(enter) + Vector3.up * pegHoverHeight);
// Try to find a suitable slot to drop the peg:
TangledPegSlot closest = FindCandidateSlot(selectedPeg);
// If we found a candidate slot, tint it to give the player some visual feedback:
if (closest != null)
closest.Tint();
// Drop selected peg:
if (Input.GetMouseButtonUp(0))
{
// If we could find a free slot nearby, connect to it:
if (closest != null)
{
selectedPeg.currentSlot = null;
selectedPeg.DockInSlot(closest);
closest.ResetColor();
}
else // If we couldn't find one or if it's too far, return to current slot.
{
selectedPeg.DockInSlot(selectedPeg.currentSlot);
}
selectedPeg = null;
}
}
// If all ropes have been contact-free for a certain amount of frames, trigger finish event.
if (framesSinceLastContact >= framesWithoutContactsToWin && onFinish != null)
onFinish.Invoke();
}
private void Solver_OnParticleCollision(ObiSolver s, ObiNativeContactList e)
{
// Count contacts between different ropes (that is, exclude self-contacts):
int contactsBetweenRopes = 0;
for (int i = 0; i < e.count; ++i)
{
var ropeA = s.particleToActor[s.simplices[e[i].bodyA]].actor;
var ropeB = s.particleToActor[s.simplices[e[i].bodyB]].actor;
if (ropeA != ropeB)
contactsBetweenRopes++;
}
// If there's no contacts, bump the amount of frames we've been contact-free.
// Otherwise reset the amount of frames to zero.
if (contactsBetweenRopes == 0)
framesSinceLastContact++;
else
framesSinceLastContact = 0;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c1cdc16773c1f4ebe94e32c3ae0005bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,114 @@
using UnityEngine;
namespace Obi.Samples
{
public class VineClimbController : MonoBehaviour
{
public ObiSolver solver;
public float climbSpeed = 1.5f;
ObiPinhole pinhole;
bool pressedSpace = false;
void Start()
{
solver.OnCollision += Solver_OnCollision;
}
public void Update()
{
if (pinhole != null)
{
float speed = 0;
if (Input.GetKey(KeyCode.W))
speed = climbSpeed;
if (Input.GetKey(KeyCode.S))
speed = -climbSpeed;
pinhole.motorSpeed = speed;
}
if (Input.GetKeyDown(KeyCode.Space))
{
pressedSpace = true;
DetachFromVine();
}
}
private void Solver_OnCollision(ObiSolver solver, ObiNativeContactList contacts)
{
if (pressedSpace)
{
int bestParticle = -1;
float closestDistance = float.MaxValue;
Vector3 closestOffset = Vector3.zero;
foreach (var c in contacts)
{
if (c.distance < 0.001f)
{
ObiCollider collider = ObiColliderWorld.GetInstance().colliderHandles[c.bodyB].owner as ObiCollider;
if (collider.sourceCollider.isTrigger)
{
int particle = solver.simplices[c.bodyA];
var worldPosition = solver.transform.TransformPoint(solver.positions[particle]);
Vector3 offset = worldPosition - collider.transform.position;
float distance = Vector3.Magnitude(offset);
if (distance < closestDistance)
{
closestDistance = distance;
closestOffset = offset;
bestParticle = particle;
}
}
}
}
if (bestParticle >= 0)
{
var actor = solver.particleToActor[bestParticle].actor;
AttachToVine(actor as ObiRope, bestParticle, closestOffset);
}
pressedSpace = false;
}
}
private float GetParticleMu(ObiRope rope, int solverParticleIndex)
{
for (int i = 0; i < rope.elements.Count; ++i)
{
if (rope.elements[i].particle1 == solverParticleIndex)
return i / (float)rope.elements.Count;
if (rope.elements[i].particle2 == solverParticleIndex)
return (i + 1) / (float)rope.elements.Count;
}
return 1;
}
// Update is called once per frame
void AttachToVine(ObiRope rope, int particle, Vector3 offset)
{
if (pinhole == null && rope != null)
{
transform.position += offset;
pinhole = rope.gameObject.AddComponent<ObiPinhole>();
pinhole.position = GetParticleMu(rope, particle);
pinhole.motorForce = Mathf.Infinity;
pinhole.friction = 1;
pinhole.target = this.transform;
}
}
void DetachFromVine()
{
if (pinhole != null)
{
GameObject.Destroy(pinhole);
pinhole = null;
pressedSpace = false;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 50c674b3f8f0045249ac099130c6a6b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,74 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace Obi.Samples
{
[RequireComponent(typeof(ObiSolver))]
public class WrapRopeGameController : MonoBehaviour
{
ObiSolver solver;
public Wrappable[] wrappables;
public UnityEvent onFinish = new UnityEvent();
private void Awake()
{
solver = GetComponent<ObiSolver>();
}
// Start is called before the first frame update
void OnEnable()
{
solver.OnCollision += Solver_OnCollision;
}
private void OnDisable()
{
solver.OnCollision -= Solver_OnCollision;
}
private void Update()
{
bool allWrapped = true;
// Test our win condition: all pegs must be wrapped.
foreach (var wrappable in wrappables)
{
if (!wrappable.IsWrapped())
{
allWrapped = false;
break;
}
}
if (allWrapped)
onFinish.Invoke();
}
private void Solver_OnCollision(ObiSolver s, ObiNativeContactList e)
{
// reset to unwrapped state:
foreach (var wrappable in wrappables)
wrappable.Reset();
var world = ObiColliderWorld.GetInstance();
foreach (Oni.Contact contact in e)
{
// look for actual contacts only:
if (contact.distance < 0.025f)
{
var col = world.colliderHandles[contact.bodyB].owner;
if (col != null)
{
var wrappable = col.GetComponent<Wrappable>();
if (wrappable != null)
wrappable.SetWrapped();
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 92b1832c67f564e9fa43a180598111e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,45 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
[RequireComponent(typeof(Rigidbody))]
public class WrapRopePlayerController : MonoBehaviour
{
public float acceleration = 50;
Rigidbody rb;
void Awake()
{
rb = GetComponent<Rigidbody>();
}
// Update is called once per frame
void Update()
{
Vector3 direction = Vector3.zero;
// Determine movement direction:
if (Input.GetKey(KeyCode.W))
{
direction += Vector3.up * acceleration;
}
if (Input.GetKey(KeyCode.A))
{
direction += Vector3.left * acceleration;
}
if (Input.GetKey(KeyCode.S))
{
direction += Vector3.down * acceleration;
}
if (Input.GetKey(KeyCode.D))
{
direction += Vector3.right * acceleration;
}
rb.AddForce(direction.normalized * acceleration, ForceMode.Acceleration);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fdf58ad80e1cd4860aa12e8da0aa833c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Obi.Samples
{
public class Wrappable : MonoBehaviour
{
private bool wrapped = false;
public Color normalColor = new Color(0.2f, 0.2f, 0.8f);
public Color wrappedColor = new Color(0.9f, 0.9f, 0.2f);
Material localMaterial;
public void Awake()
{
localMaterial = GetComponent<MeshRenderer>().material;
}
public void OnDestroy()
{
Destroy(localMaterial);
}
public void Reset()
{
wrapped = false;
localMaterial.color = normalColor;
}
public void SetWrapped()
{
wrapped = true;
localMaterial.color = wrappedColor;
}
public bool IsWrapped()
{
return wrapped;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39f0f8f454a2049f3af7bad3dda0481e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: