Each bubble is a disc of trapped air of radius r, so its 2D "volume" (area per unit depth) is A = πr². Two real forces act on it: weight W = ρair·A·g pulling down, and buoyancy (Archimedes) Fb = ρfluid·A·g pushing up, since it displaces fluid far denser than the air inside it.
Accelerating through the fluid also has to shove fluid out of the way, which resists the bubble's own acceleration — the classic added-mass effect: meff = Cm·ρfluid·A (Cm≈0.5 for a sphere). Opposing motion is quadratic (form) drag, Fd = ½·Cd·ρfluid·(2r)·v|v|. Balancing buoyancy against drag gives a terminal rise speed
v_term = sqrt( pi * g * r * (rho_fluid - rho_air) / (Cd * rho_fluid) )
so bigger bubbles genuinely rise faster (v ∝ √r), exactly as real bubbles do — not a scripted speed-by-size lookup, but the outcome of integrating F = m·a every frame.
When two bubbles' discs overlap, an elastic/inelastic impulse along the line of centers separates them and exchanges momentum (restitution e). If they stay pressed together with overlap deeper than a size-dependent threshold, the thin film between them is treated as draining under its own Laplace pressure P = γ/r — smaller bubbles have higher internal pressure, drain faster, and pop the shared film sooner — so a coalescence chance accumulates each frame and, once triggered, the two merge into one bubble whose area equals the sum of both (r_new = √(r₁²+r₂²)) with momentum conserved from their actual gas masses.