224 lines
8.1 KiB
C#
224 lines
8.1 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using Obi;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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 = 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, float.PositiveInfinity);
|
|
batch.AddConstraint(rope.elements[rope.elements.Count-1].particle2, hookAttachment.collider.GetComponent<ObiColliderBase>(),
|
|
hookAttachment.collider.transform.InverseTransformPoint(hookAttachment.point), Quaternion.identity, 0, 0, float.PositiveInfinity);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|