Create a procedurally generated basketball hoop in Unity with the following specifications:
- 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 atz = 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 bothmainTextureand_BaseMap. Set_BaseColorand.colorto white on the Quad material so the texture renders unmodified. The cube material uses_BaseColorset 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 aty = 0in 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 atz = -(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-convexMeshCollider(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
rimRadiusat the top torimRadius * 0.35at the bottom over a depth of 0.45m. Each pair of connected nodes has a LineRenderer usinguseWorldSpace = falseand theUnlit/Colorshader (respects depth testing so lines render behind the rim). Line width: 0.003m, color: white.
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)whereb = a + tubeSegs + 1
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():
- Hoop movement tracking: Compute
hoopDelta = transform.position - prevHoopPos. If the hoop moved, shift ALLsimPos[]andsimPrev[]by the delta. This keeps the simulation in sync when the hoop is moved at runtime. - Fixed node sync: Fixed nodes (top row) update their
simPosfrom their transform's world position each frame, so they follow the rim. - Verlet step: For each non-fixed node:
newPos = pos + (pos - prevPos) * (1 - damping) + gravity * dt^2 - 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).
- Ball collision: Use
Physics.OverlapSpherecentered on the net area. For eachSphereColliderfound, push net nodes out of the ball's volume and apply a reaction force on the ball'sRigidbody. - Transform update: Write
simPos[]back tonetNodeTr[i].position.
Line positions are updated in LateUpdate() using netNodeTr[i].localPosition (local space since LineRenderers use useWorldSpace = false).
- 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)
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.
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(notnode.position) since both the LineRenderer and nodes share the same Net parent. - In
LateUpdate(), update lines withnetNodeTr[i].localPosition.
ProceduralBasketballHoop.cs— Main MonoBehaviour that generates the hoop inStart()and runs the Verlet simulation inFixedUpdate()/LateUpdate()NetLineUpdater.cs— Unused legacy script from early joint-based approach. Can be removed.
- Use
DestroyImmediate()to clear children —Destroy()is deferred to end-of-frame, causing duplicate physics objects when entering Play mode with editor-previewed children. - Use
sharedMaterial(notmaterial) to avoid material leak warnings in edit mode. Create materials once inBuild()and assign viasharedMaterial. - Detect render pipeline:
Shader.Find("Universal Render Pipeline/Lit")with fallback to"HDRP/Lit"then"Standard". Set both_BaseColorand.colorfor compatibility. - Use
Unlit/Colorshader for LineRenderers —Sprites/Defaultignores the depth buffer and renders on top of everything (net lines visible through the rim). - 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. SetParent(parent, false)for ALL child objects — Withoutfalse, 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.- 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 bothmaterial.mainTextureandmaterial.SetTexture("_BaseMap", tex)to cover both Standard and URP shaders. - 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.1524so the rim appears near the bottom, not the center. The shooter's square bottom edge aligns with rim level at(6/42) * textureHeightpixels from the bottom of the texture. - 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.
- Hoop movement support — Track
prevHoopPosand apply the delta to all simulation positions inFixedUpdate(). Fixed nodes additionally sync from their transform each frame. LineRenderers use local space so they follow the parent in the editor automatically.
| 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 |