Skip to content

Hose system

Assets/Scripts/Hose, namespace HoseBoy. Twelve components plus a ScriptableObject and one interface. Prefabs live in Assets/Prefabs/Hose (HoseRig.prefab, WaterBackpack.prefab).

Half of this is not in a scene

HoseChain, HoseNozzle, HoseWaterJet, HoseWaterStream, HoseAimReticle, BoneAttachment and HoseRecoil are wired into HoseRig.prefab and/or Scenes/demo.unity. HoseTank, HoseArmAim, HoseFluidVisuals and FluidProfile are referenced from nowhere — no scene, no prefab, and no FluidProfile asset exists. HoseWaterJet calls tank.Draw(...) only when a tank is assigned, so today the jet has infinite water.

Execution order

The order is load-bearing. Each component reads a pose the previous one wrote, and getting this wrong produces a hose attached to last frame's hand.

Order Component Reason
−10 HelloMarioFramework.BoneFollower retargets the framework skeleton onto the monkey
−5 FirstPersonLook writes the FP rig before CinemachineBrain samples it
−4 HoseArmAim two-bone IK, after retarget, before anything reads the hand
−3 TrueFirstPersonBody collapses the head after everything else has posed it
0 CinemachineBrain forced to LateUpdate by FreeLookHelper.Awake
90 BoneAttachment snaps the rig to the hand and back bones
100 HoseNozzle, HoseChain aim and rope simulation from the final bone pose
110 HoseWaterJet traces the arc from the settled aim
120 HoseAimReticle places the marker on the traced impact
200 HoseRecoil after Player.FixedUpdate writes velocity

Data flow

flowchart TD
    Bone[BoneAttachment<br/>order 90] --> Nozzle[HoseNozzle<br/>order 100]
    Bone --> Chain[HoseChain<br/>order 100]
    Nozzle -->|AimDirection| Jet[HoseWaterJet<br/>order 110]
    Nozzle -->|AimDirection| Arm[HoseArmAim<br/>order -4]
    Tank[HoseTank] -->|Draw / CanDraw| Jet
    Profile[FluidProfile<br/>ScriptableObject] --> Tank
    Jet -->|path, pressure| Stream[HoseWaterStream]
    Jet -->|impact| Reticle[HoseAimReticle<br/>order 120]
    Jet -->|Throttle, AimDirection| Recoil[HoseRecoil<br/>order 200]
    Jet -->|OnWaterHit| Target[IWaterTarget]
    Tank -->|FluidChanged| Visuals[HoseFluidVisuals]
    Tank -->|Level01| Meter[FluidMeterUI]
    Profile --> Visuals
    Profile --> Meter

Components

HoseChain

The hose itself: a single procedurally generated SkinnedMeshRenderer — one draw call — driven by a Verlet chain between the supply anchor and the nozzle anchor.

LateUpdate runs SpoolSampleGround → 1–4 sub-stepped SimulateApplyToBonesUpdateBoundsPushUVStretch. Simulate integrates, then per iteration applies distance constraints with both ends pinned, bend smoothing by stiffness, a clearance sphere pushing the rope off the backpack, and a ground clamp that also scrubs momentum out of the previous-position array so the hose settles instead of skating.

Build() (also exposed as a Rebuild Hose context-menu item) creates boneCount hidden bone GameObjects and builds a capped tube with a duplicated seam vertex and two-bone weights blended 0.7 / 0.3 per ring.

PushUVStretch writes _UVStretch through a MaterialPropertyBlock, consumed by Assets/Art/Shaders/ToonHose.shader.

Key fields: boneCount 4–48 (default 20), radius 0.06, sides 3–16 (10), length 8, slack 1.35, spoolSpeed 7, gravity −14, drag 0.06, iterations 1–8 (4), stiffness 0.25, anchorClearance 0.3, groundFriction 0.65. Public API: ResetToAnchors(), Build().

HoseNozzle

Aiming. Takes aimSource.forward — falling back to Camera.main, then to the parent's forward — clamps pitch to [−70, 80], slerps towards it at turnSpeed 14 in a frame-rate-independent way, and applies an aimOffset.

Public: Vector3 AimDirection, Transform Muzzle. Everything downstream aims off AimDirection, so there is exactly one aim vector in the system.

HoseWaterJet

The jet. Each LateUpdate it re-enables its input action (menus and scene loads can steal it), reads IsPressed(), asks the tank for water, ramps the throttle, traces the arc, and updates the stream mesh, particles, audio and impact.

tank.Draw(throttle, dt) returning false is what cuts the jet. That single return value is the entire pressure economy from the jet's point of view.

TracePath applies Perlin-noise aim jitter, then walks a ballistic arc in pathPoints segments with a Physics.Linecast per step, filling the path buffer and recording the impact. ApplyImpact does AddForceAtPosition scaled by the fluid's impactMultiplier, then calls OnWaterHit on a cached IWaterTarget lookup.

Key fields: speed 16, dropGravity −11, range 14, pathPoints 4–32 (14), aimJitter 1.5°, spinUp 0.12, spinDown 0.22, pushForce 7, maxVolume 0.7, plus a sprayWithoutInput test toggle. Public: Throttle, AimDirection, IsSpraying, Tank, SetSpraying(bool), TryGetImpact(out Vector3 point, out Vector3 normal).

HoseWaterStream

The toon jet mesh. Allocates a fixed index buffer once; UpdateStream(path, count, pressure) wraps a tube around the traced path using a parallel-transport frame so it does not spin as the aim swings, adds a travelling sine bulge, tapers the tip, and scales radius by pressure. Rings past count collapse to zero radius rather than resizing the mesh, so there is no per-frame allocation.

HoseTank

The supply. Drains while drawing, refills on a delay, and reads every number off its FluidProfile — which is the whole point: the castle can hand the player a lava outlet and nothing in this class changes.

public bool Draw(float throttle, float deltaTime)

Returns false once the tank cannot keep up. With lockoutWhenEmpty on, running dry latches the lockout and the tank will not fire again until Level01 >= fluid.restartThreshold.

Public surface: Level01, CanDraw, IsRefilling, Fluid, event Action<FluidProfile> FluidChanged, SetFluid(FluidProfile, bool fillUp = true), TopUp().

SetFluid is the hook for a tap granting different water; TopUp is a refill station.

FluidProfile

[CreateAssetMenu(menuName = "HoseBoy/Fluid Profile")]. Everything that distinguishes one fluid from another, in three groups:

Group Fields
Look liquid, foam, droplets colours, streamScrollSpeed, emission
Supply capacity 100, drainPerSecond 16, refillPerSecond 22, refillDelay 0.7, restartThreshold 0.12
Feel sloshiness, recoilMultiplier, impactMultiplier

Tip

No FluidProfile asset exists in the project yet. Creating a default "Water" profile is the first step of wiring the tank up.

HoseRecoil

Shoves the boy away from wherever the hose points, for as long as it is spraying. FixedUpdate at order 200 — after Player.FixedUpdate, so the push survives the walk velocity the controller writes every physics step.

Push is -jet.AimDirection, with the upward component multiplied by upwardMultiplier 3.2 (player gravity is 75, so this has to be generous to hover at all), scaled by throttle and the fluid's recoilMultiplier. It clamps linearDamping down to sprayDrag 1.2 because the controller parks drag at 100 when idle, which would otherwise eat the push entirely, and calls player.BreakSpeedCap() above half pressure so the hose can throw the boy past his run cap.

HoseArmAim

Analytic two-bone IK on shoulder / elbow / hand, keeping the real arm pointed where the nozzle aims. There is no viewmodel in first person, so the arm you see holding the nozzle has to actually hold it.

Bend angles come from the law of cosines, the bend plane comes from an elbowHint transform, then the whole limb is swung with a FromToRotation so the hand lands on the target. The target is shoulder + aim * reach plus a holdOffset in the aim's own space, which keeps the nozzle out of the middle of the view.

SetWeight(float) is called by the camera director — full weight in first person, reduced in third — and blends over blendTime.

HoseAimReticle

Lays a marker flat on the surface the arc actually lands on, not straight ahead. Reads jet.TryGetImpact, scales with distance and throttle, lifts off the surface to avoid z-fighting, and spins slowly. SetSuppressed(bool) drops it in first person, where the HUD crosshair takes over.

HoseFluidVisuals

Retints the stream renderer (_Color, _FoamColor, _ScrollSpeed via a MaterialPropertyBlock) and both particle systems whenever HoseTank.FluidChanged fires. Keeps fluid appearance in the profile rather than baked into materials.

FluidMeterUI

Assets/Scripts/UI/FluidMeterUI.cs. The corner gauge. Instances its own material copy in Awake because the canvas batches by material.

Level is smoothed towards tank.Level01 over levelLag. Slosh is a spring: lateral acceleration of the motionSource rigidbody, measured against the camera's right vector, tips the surface; settleSpring pulls it back and settleDamping kills it, and the overshoot is what reads as sloshing. Draining adds drainAgitation to the wave amplitude. Below lowAt the image tint pulses towards lowTint.

Shader properties driven: _Fill, _Tilt, _Liquid, _Foam, _Emission, _WaveAmp.

BoneAttachment

Sticks a transform to an animated bone without parenting, so the hose rig stays one self-contained prefab instead of being scattered through the character skeleton. Resolves the bone by name under a search root if no explicit target is assigned, and applies local-space position and rotation offsets in LateUpdate at order 90.

IWaterTarget

The extension point for everything the hose is supposed to affect:

void OnWaterHit(Vector3 point, Vector3 normal, Vector3 direction, float pressure);

Implement it on inlets, buttons, beetles, or anything else that should react. HoseWaterJet finds it with a cached GetComponentInParent<IWaterTarget>() on the impact collider.

Note

Nothing implements IWaterTarget yet.