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 Spool → SampleGround → 1–4 sub-stepped Simulate → ApplyToBones →
UpdateBounds → PushUVStretch. 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.
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:
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.