// GardenHose.cs // Turns a straight hose mesh into a rubber hose that runs from a reel (or a tap, a pump, a fuel station) to a nozzle a // player can pick up. The hose hangs and sags, bends, stretches like rubber when pulled, lies on the ground and slides // over things. Let go of the nozzle and it goes back on its hook after a moment. // Every viewer simulates the hose from the synced reel and nozzle positions, so everyone sees it the same way with // no extra network traffic; players who join later see it too. // // Setup: // 1. Import a hose model, modelled STRAIGHT (a long thin tube along one axis, a few metres long). The more length // segments (rings) it has, the smoother it bends. Place it anywhere, and turn Enable Collision OFF on it. // 2. Add a Reel object (any mesh or an empty) where the hose starts. // 3. Add a Nozzle object: tick Grabbable on it (leave Use Rigidbody off so it stays on the hook). // 4. Add a Hook object (an empty, or a small mesh) where the nozzle hangs up. Put the nozzle on the hook. // 5. Put this script on any object and slot Hose, Reel, Nozzle and Hook in the Inspector. // 6. Play: grab the nozzle and walk around with it. Tune Stiffness / Stretch live, or with chat commands: // /hose drop (let go of the end: it drops and hangs from the reel) // /hose attach (put the end back on the nozzle) // /hose hook (put the end on the hook instead of the nozzle) // /hose stiff 0.6 /hose stretch 0.5 /hose slack 1.6 /hose off /hose on // // The offsets put each hose end on a point of that object (in its own local space), e.g. the back of the nozzle. using System.Collections; using IHXR.Simulation; public class GardenHose : ScriptBase { [Header("Objects")] [Tooltip("The hose mesh: modelled straight, along its longest axis. Turn its Enable Collision OFF.")] public SceneObject Hose; [Tooltip("Where the hose starts (the anchor): a reel, a tap, a pump.")] public SceneObject Reel; [Tooltip("A local point on the reel the hose comes out of.")] public Vector3 ReelOffset = Vector3.zero; [Tooltip("The nozzle players pick up (Grabbable on). The hose end follows it.")] public SceneObject Nozzle; [Tooltip("A local point on the nozzle the hose plugs into (e.g. its back end).")] public Vector3 NozzleOffset = Vector3.zero; [Tooltip("Where the nozzle hangs up when nobody holds it.")] public SceneObject Hook; [Header("Hose feel")] [Tooltip("How much it resists bending: 0 = limp rope, 1 = stiff pipe.")] [Range(0f, 1f)] public float Stiffness = 0.3f; [Tooltip("How rubbery it is when pulled: 0 = does not stretch, 1 = very stretchy.")] [Range(0f, 1f)] public float Stretch = 0.3f; [Tooltip("How far it may stretch at most (2 = double its length).")] public float MaxStretch = 2f; [Tooltip("Friction on the ground: 0 = slippery, 1 = grippy.")] [Range(0f, 1f)] public float Friction = 0.6f; [Tooltip("Smoothness of the curve (4 to 96). More = smoother, a little more work for every viewer.")] public int Segments = 24; [Tooltip("How much longer than the gap between reel and nozzle the hose is, so it hangs and curls (1 = taut, 1.5 = lots of slack). Used when Length is 0.")] public float Slack = 1.4f; [Tooltip("Length in metres. 0 = automatic: the model's own length, or Slack x the gap between reel and nozzle if that is longer.")] public float Length = 0f; [Tooltip("The hose leaves the nozzle along this local direction (0,0,0 = free). E.g. (0,0,-1) = out of its back.")] public Vector3 NozzleDirection = Vector3.zero; [Header("Hang up")] [Tooltip("Put the nozzle back on the hook after it is let go.")] public bool ReturnToHook = true; [Tooltip("Seconds after letting go before it goes back on the hook.")] public float ReturnDelay = 2f; public bool ChatCommands = true; private int releaseCount; public override void Start() { base.Start(); if (!IsValid(Hose) || !IsValid(Reel)) { LogError("Slot the Hose and the Reel."); return; } BuildHose(); if (IsValid(Nozzle)) ListenForGrab(Nozzle, OnNozzleGrab); if (ChatCommands) RegisterChatCommand(OnChat); } public override void OnDestroy() { if (IsValid(Hose)) StopHose(Hose); } private HoseSettings Settings() { return new HoseSettings { Stiffness = Stiffness, Stretch = Stretch, MaxStretch = MaxStretch, Friction = Friction, Segments = Segments, Length = Length, Slack = Slack, EndDirection = NozzleDirection }; } private void BuildHose() { // Start on the reel, end on the nozzle (or on the hook when there is no nozzle). SceneObject end = IsValid(Nozzle) ? Nozzle : Hook; MakeHose(Hose, Reel, ReelOffset, end, IsValid(Nozzle) ? NozzleOffset : Vector3.zero, Settings()); } private void OnNozzleGrab(GrabInfo grab) { if (grab.IsGrabbed) { releaseCount++; return; } // picked up: the hose end follows the hand if (ReturnToHook && IsValid(Hook)) StartCoroutine(BackOnHook(++releaseCount)); } private IEnumerator BackOnHook(int release) { yield return new WaitForSeconds(Mathf.Max(0f, ReturnDelay)); if (release != releaseCount || GetHolder(Nozzle) != "") yield break; // picked up again meanwhile Nozzle.transform.position = Hook.transform.position; Nozzle.transform.eulerAngles = Hook.transform.eulerAngles; } private void OnChat(ChatCommandInfo chat) { string text = (chat.FullText ?? "").Trim().ToLowerInvariant(); if (!text.StartsWith("/hose")) return; string[] parts = text.Split(' '); string cmd = parts.Length > 1 ? parts[1] : ""; float value = 0f; bool hasValue = parts.Length > 2 && float.TryParse(parts[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out value); if (cmd == "drop") { DetachHoseEnd(Hose); SendScriptChat("Hose end dropped"); } else if (cmd == "attach" && IsValid(Nozzle)) { SetHoseEnd(Hose, Nozzle, NozzleOffset); SendScriptChat("Hose on the nozzle"); } else if (cmd == "hook" && IsValid(Hook)) { SetHoseEnd(Hose, Hook); SendScriptChat("Hose on the hook"); } else if (cmd == "stiff" && hasValue) { Stiffness = Mathf.Clamp(value, 0f, 1f); BuildHose(); SendScriptChat("Stiffness: " + Stiffness); } else if (cmd == "slack" && hasValue) { Slack = Mathf.Max(1f, value); BuildHose(); SendScriptChat("Slack: " + Slack); } else if (cmd == "stretch" && hasValue) { Stretch = Mathf.Clamp(value, 0f, 1f); BuildHose(); SendScriptChat("Stretch: " + Stretch); } else if (cmd == "off") { StopHose(Hose); SendScriptChat("Hose off (back to its modelled shape)"); } else if (cmd == "on") { BuildHose(); SendScriptChat("Hose on"); } } private static bool IsValid(SceneObject o) { return o != null && !o.IsNull; } }