Skip to content

Instantly share code, notes, and snippets.

@dilmerv
Last active May 3, 2026 14:30
Show Gist options
  • Select an option

  • Save dilmerv/8014d2714ade3b61d45a9a0aa94a38a8 to your computer and use it in GitHub Desktop.

Select an option

Save dilmerv/8014d2714ade3b61d45a9a0aa94a38a8 to your computer and use it in GitHub Desktop.
BasketballHoopPlan.md

Procedural Basketball Hoop - Recreation Plan

Prompt

Create a procedurally generated basketball hoop in Unity with the following specifications:

Structure

  • Backboard: A cube (1.8m x 1.05m x 0.04m) using a plain glass-tint material (0.82, 0.87, 0.93) with NO texture — this keeps the back and sides clean. A separate child Quad ("BackboardFace") is overlaid on the front face only, positioned at z = backboardZ - backboardThickness/2 - 0.001 (flush with the front surface, slight offset to prevent z-fighting), rotated 180° around Y to face the rim/player. The Quad's collider is removed. The Quad carries the procedurally generated texture: light blue-grey glass tint fill, white outer border, and a centered shooter's square (24"x18" proportional to a 72"x42" board) with its bottom edge at rim level (6" from board bottom). Use the URP Lit shader with the texture applied to both mainTexture and _BaseMap. Set _BaseColor and .color to white on the Quad material so the texture renders unmodified. The cube material uses _BaseColor set to the glass tint color directly. Attach a kinematic Rigidbody to the cube only.
  • Backboard Positioning: The backboard is offset upward so the rim sits near its bottom edge, matching regulation placement. backboardYOffset = backboardHeight / 2 - 0.1524 (6 inches / 0.1524m is the regulation distance from rim center to bottom edge of backboard). The rim and bracket remain at y = 0 in the hoop's local space.
  • Bracket: A small cube connecting the backboard to the rim, spanning only the gap between the backboard and the rim's back edge (length = backboardOffset). Centered at z = -(rimRadius + backboardOffset / 2). Colored orange to match the rim. Must NOT reach into the hoop opening. Attach a kinematic Rigidbody.
  • Rim: A single procedural torus mesh generated by BuildTorusMesh(rimRadius=0.23, tubeRadius=0.012, 32 ring segments, 12 tube segments). The torus lies flat in the XZ plane. Use a non-convex MeshCollider (a convex hull would fill the hole and block balls). Attach a kinematic Rigidbody. Color: orange (1, 0.35, 0).
  • Net: 12 columns x 7 rows of empty GameObjects (no Rigidbody, no Collider) arranged in a diamond mesh pattern. Top row (row 0) is fixed to the rim positions. Odd rows are offset by half a column angle. The net tapers from rimRadius at the top to rimRadius * 0.35 at the bottom over a depth of 0.45m. Each pair of connected nodes has a LineRenderer using useWorldSpace = false and the Unlit/Color shader (respects depth testing so lines render behind the rim). Line width: 0.003m, color: white.

Torus Mesh Generation

BuildTorusMesh(float R, float r, int ringSegs, int tubeSegs)
  • Vertices: (ringSegs+1) * (tubeSegs+1) arranged on the torus surface
  • For each ring position theta: center = (cos(theta)*R, 0, sin(theta)*R), radial direction = (cos(theta), 0, sin(theta))
  • For each tube position phi: vertex = center + (radial*cos(phi) + up*sin(phi)) * r
  • Triangle winding (outward-facing in Unity): (a, a+1, b) and (b, a+1, b+1) where b = a + tubeSegs + 1

Net Physics (Verlet Integration)

Do NOT use Unity Rigidbodies or joints on net nodes. Unity's joint system (SpringJoint, ConfigurableJoint) fails catastrophically when nodes have multiple competing constraints — each net node has 2-4 connections, causing the solver to explode.

Use position-based Verlet integration in FixedUpdate():

  1. Hoop movement tracking: Compute hoopDelta = transform.position - prevHoopPos. If the hoop moved, shift ALL simPos[] and simPrev[] by the delta. This keeps the simulation in sync when the hoop is moved at runtime.
  2. Fixed node sync: Fixed nodes (top row) update their simPos from their transform's world position each frame, so they follow the rim.
  3. Verlet step: For each non-fixed node: newPos = pos + (pos - prevPos) * (1 - damping) + gravity * dt^2
  4. Constraint solving: 8 iterations of distance constraints. For each constraint: compute correction to bring connected nodes back to their rest length, split 50/50 between both nodes (skip fixed nodes).
  5. Ball collision: Use Physics.OverlapSphere centered on the net area. For each SphereCollider found, push net nodes out of the ball's volume and apply a reaction force on the ball's Rigidbody.
  6. Transform update: Write simPos[] back to netNodeTr[i].position.

Line positions are updated in LateUpdate() using netNodeTr[i].localPosition (local space since LineRenderers use useWorldSpace = false).

Diamond Net Connection Pattern

  • Even rows: node (r, c) connects to (r+1, c) and (r+1, (c-1+N)%N)
  • Odd rows: node (r, c) connects to (r+1, c) and (r+1, (c+1)%N)
  • This produces 144 total connections (12 columns x 6 row gaps x 2 connections each)

Child Parenting — Always Use SetParent(parent, false)

ALL child objects (Backboard, Bracket, Rim, Net parent, net nodes, LineRenderer objects) must use SetParent(parent, false). The false parameter keeps the child's local transform at identity (position zero, rotation zero, scale one) instead of preserving its world position/rotation. Without false:

  • Rotation breaks: If the hoop is rotated, children counter-rotate to maintain world rotation 0 — the backboard stays axis-aligned instead of rotating with the hoop.
  • Position offsets: If the hoop isn't at the origin, children get non-zero local offsets that misplace them.

After SetParent(_, false), explicitly set localPosition and localScale as needed.

LineRenderer Local Space Setup

LineRenderers must use local space (useWorldSpace = false) so the net moves with the hoop in the editor without needing a running simulation:

  • Parent LineRenderer objects with SetParent(parent, false) (same rule as above).
  • Set positions using node.localPosition (not node.position) since both the LineRenderer and nodes share the same Net parent.
  • In LateUpdate(), update lines with netNodeTr[i].localPosition.

Scripts Required

  1. ProceduralBasketballHoop.cs — Main MonoBehaviour that generates the hoop in Start() and runs the Verlet simulation in FixedUpdate() / LateUpdate()
  2. NetLineUpdater.cs — Unused legacy script from early joint-based approach. Can be removed.

Key Lessons / Gotchas

  1. Use DestroyImmediate() to clear childrenDestroy() is deferred to end-of-frame, causing duplicate physics objects when entering Play mode with editor-previewed children.
  2. Use sharedMaterial (not material) to avoid material leak warnings in edit mode. Create materials once in Build() and assign via sharedMaterial.
  3. Detect render pipeline: Shader.Find("Universal Render Pipeline/Lit") with fallback to "HDRP/Lit" then "Standard". Set both _BaseColor and .color for compatibility.
  4. Use Unlit/Color shader for LineRenderersSprites/Default ignores the depth buffer and renders on top of everything (net lines visible through the rim).
  5. Torus MeshCollider must be non-convex (convex = false). A convex hull of a torus fills the hole and blocks balls. Non-convex is safe because the rim Rigidbody is kinematic.
  6. SetParent(parent, false) for ALL child objects — Without false, Unity preserves world position/rotation. This causes two bugs: (a) children get non-zero local offsets that misplace them when the hoop isn't at the origin, and (b) children counter-rotate to maintain world rotation 0, so rotating the hoop doesn't rotate the backboard/bracket/rim. Apply to Backboard, Bracket, Rim, Net parent, net nodes, and LineRenderer objects.
  7. Backboard texture on front face only: Do NOT apply the texture to the cube directly — it renders on all 6 faces (back, sides, top, bottom). Instead, use a separate Quad child overlaid on the front face with the textured material. The cube gets a plain tint-colored material. Use Texture2D.SetPixels() with a pixel array. Draw rectangles by iterating edges. Set both material.mainTexture and material.SetTexture("_BaseMap", tex) to cover both Standard and URP shaders.
  8. Regulation backboard positioning: The rim center sits 6 inches (0.1524m) above the bottom edge of the backboard. Offset the backboard upward by backboardHeight/2 - 0.1524 so the rim appears near the bottom, not the center. The shooter's square bottom edge aligns with rim level at (6/42) * textureHeight pixels from the bottom of the texture.
  9. Joint-based net physics does NOT work — SpringJoint: spring-to-mass ratio causes simulation instability. ConfigurableJoint with Locked motion: competing constraints on multi-connected nodes cause solver explosion. ConfigurableJoint with drives: still falls apart. Verlet integration is the only approach that works reliably.
  10. Hoop movement support — Track prevHoopPos and apply the delta to all simulation positions in FixedUpdate(). Fixed nodes additionally sync from their transform each frame. LineRenderers use local space so they follow the parent in the editor automatically.

Default Parameters

Parameter Value Description
rimRadius 0.23 Regulation ~46cm diameter
rimTubeRadius 0.012 Rim tube thickness
backboardWidth 1.8 Regulation width
backboardHeight 1.05 Regulation height
backboardOffset 0.05 Gap between rim back and backboard
netColumns 12 Nodes per ring
netRows 7 Rings including fixed top row
netDepth 0.45 Vertical net length
bottomRadiusRatio 0.35 Net taper (fraction of rimRadius)
nodeColliderRadius 0.008 Virtual node size for ball collision
netLineWidth 0.003 LineRenderer width
solverIterations 8 Verlet constraint passes per frame
netDamping 0.02 Velocity damping per frame
ballPushStrength 0.8 How strongly the ball displaces net nodes
rimColor (1, 0.35, 0) Orange
netColor White
backboardColor (0.85, 0.85, 0.9) Light blue-grey
NET_LAYER 8 Layer for self-collision ignore
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment