Skip to content

Coding conventions

These describe what the existing code in Assets/Scripts actually does. Match it.

Namespaces

Namespace Contents
HoseBoy everything written for this game — Camera, Hose, UI
Monologue.* the vendored dialogue framework
HelloMarioFramework the vendored controller kit
BuildingBlocks.DataTypes InspectableDictionary

New game code goes in HoseBoy. A handful of files in Assets/Scripts/Utils sit in the global namespace (CinemachineCameraManager, DontDestroyOnLoad and friends); that is a wart, not a pattern to follow.

Naming

  • PascalCase for types, methods, properties and events.
  • camelCase for private fields and locals. No m_ or _ prefix on new code — the hose and camera scripts use bare camelCase for privates, and that is the house style. (Monologue uses m_ and _; leave it alone but do not copy it.)
  • Serialised private fields are still camelCase; Unity's inspector already prettifies them.
  • Constants and static readonly shader IDs: PascalCase with an Id suffix — private static readonly int FillId = Shader.PropertyToID("_Fill");

Component style

Private fields with [SerializeField], not public fields. Expose read-only state as properties:

[SerializeField]
private HoseWaterJet jet;

public float Throttle => throttle;

Group the inspector with [Header] and explain with [Tooltip]. Every serialised field that a designer will touch gets a tooltip saying what the number means in units, not what the field is called again:

[SerializeField, Tooltip("Recoil acceleration at full pressure, in metres per second squared.")]
private float acceleration = 22f;

Resolve references in Awake with a sensible fallback, so a component dropped on an object mostly works before anyone opens the inspector:

void Awake()
{
    if (jet == null) jet = GetComponentInChildren<HoseWaterJet>();
}

Null-guard every serialised reference at the point of use. Components in this project are routinely half-wired; nothing should throw because a field is empty.

Use [RequireComponent] when a component genuinely cannot work without another, and implement Reset() to set sane defaults (CameraZone sets its collider to trigger).

Execution order is explicit

Anything whose correctness depends on running before or after something else declares it:

[DefaultExecutionOrder(200)] // After Player.FixedUpdate writes velocity
public class HoseRecoil : MonoBehaviour

The attribute needs a comment saying what it is ordered against. The full ladder is in Hose system; add new entries there.

Comments

The existing code comments why, not what, and it is worth preserving:

// The canvas batches by material, so this gets its own copy to write into
instance = new Material(image.material);

Every non-obvious component has an XML <summary> explaining what it is for in one or two sentences, and a <remarks> block when the execution order or a workaround needs justifying. Write those for new components. Do not write comments that restate the line below them.

Flag known problems in place with // FIXME: or // TODO: and an explanation. Several already exist and are documented on the dialogue system page.

Performance habits already in the codebase

  • Allocate once. HoseWaterStream builds a fixed index buffer up front and collapses unused rings to zero radius rather than resizing the mesh per frame.
  • MaterialPropertyBlock over material instances for renderers, so batching survives. The one exception is FluidMeterUI, which must instance its material because the canvas batches by material.
  • Cache GetComponent results. HoseWaterJet caches its IWaterTarget lookup per impact collider.
  • Cache Shader.PropertyToID in static readonly int fields.
  • No GameObject.Find, no SendMessage, no per-frame LINQ.

Cross-system communication

Static C# events, not singleton method calls, where the two systems should not know about each other. DialogueManager raises OnDialogueStartEvent; PlayerCameraDirector subscribes in OnEnable and unsubscribes in OnDisable. Neither holds a reference to the other.

Always unsubscribe in OnDisable. Static events outlive scene loads and a missed unsubscribe is a leak plus a null-target exception later.

Singletons exist (PlayerCameraDirector.Instance, DialogueManager.Instance, Player.singleton) but are used sparingly, for genuinely single-instance directors.

Extension points over special cases

Prefer an interface anything can implement to a type check. IWaterTarget is the model: the jet knows nothing about beetles, inlets or buttons, only that something on the impact collider might want to hear about being hit.

Likewise, prefer data over code. FluidProfile turns "the hose now sprays lava" into a ScriptableObject swap rather than a branch in the jet.

Formatting

Four-space indent, Allman braces, var only when the type is obvious from the right-hand side. Match the file you are in.

What not to do

  • Do not edit Assets/HelloMarioFramework or Assets/Plugins in place. Extend alongside.
  • Do not edit generated .csproj files or hoseboy.slnx.
  • Do not add public fields to reach across components; add a property or an event.
  • Do not use legacy Input.*. The one place that does is a known problem.
  • Do not use 2D physics callbacks. This is a 3D project; TriggerDialogue is the cautionary example.
  • Do not detect the player with CompareTag("Player") — the player object is untagged. Use GetComponentInParent<HelloMarioFramework.Player>().