using UnityEngine; using UnityEngine.Events; using Obi; [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().OnParticleCollision += Solver_OnParticleCollision; } private void OnDisable() { GetComponent().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; } }