Jamie Greunbaum 8eaef49f2e - Added game room, including pool and skee-ball.
- Moved video screen into its own separate movie tent.
- Adjusted stable post-processing volume.
- Chickens are now at full volume.
- Added button to toggle chickens off and on.
2026-02-09 03:49:54 -05:00

1063 lines
38 KiB
C#

using UdonSharp;
using UnityEngine;
using VRC.Udon.Common.Enums;
// ReSharper disable InconsistentNaming
// ReSharper disable CheckNamespace
namespace VRCBilliardsCE.Packages.com.vrcbilliards.vrcbce.Runtime.Scripts
{
public partial class PoolStateManager : DebuggableUdon
{
#if UNITY_ANDROID
public const float MAX_SIMULATION_TIME_PER_FRAME = 0.05f; // max time to process per frame on quest (~4)
#else
public float MAX_SIMULATION_TIME_PER_FRAME = 0.1f; // max time to process per frame on pc (~8)
#endif
public float TIME_PER_STEP = 0.0125f; // time step in seconds per iteration
private float BALL_DIAMETER_SQUARED_MINUS_EPSILON;
private float ballRadius;
private float ONE_OVER_BALL_RADIUS;
public float EARTH_GRAVITY = 9.80665f; // Earth's gravitational acceleration
private float BALL_DIAMETER_SQUARED;
private float BALL_RADIUS_SQUARED;
public float MASS_OF_BALL = 0.165f; // Weight of ball in kg
private float POCKET_INNER_RADIUS_SQUARED;
public Vector3 CONTACT_POINT = new Vector3(0.0f, -0.03f, 0.0f);
// Cue input tracking
private const float SIN_A = 0.28078832987f;
private const float COS_A = 0.95976971915f;
private const float F = 1.72909790282f;
private float accumulatedTime;
private float tableWidthMinusHeight;
private Vector3 vA;
private Vector3 vB;
private Vector3 vC;
private Vector3 vD;
private Vector3 vX;
private Vector3 vY;
private Vector3 vZ;
private Vector3 vW;
private Vector3 pK;
private Vector3 pL;
private Vector3 pN;
private Vector3 pO;
private Vector3 pP;
private Vector3 pQ;
private Vector3 pR;
private Vector3 pT;
private Vector3 pS;
private Vector3 vAvD;
private Vector3 vAvDNormal;
private Vector3 vBvY;
private Vector3 vBvYNormal;
private Vector3 vCvZNormal;
private Vector3 vAvBNormal = new Vector3(0.0f, 0.0f, -1.0f);
private Vector3 vCvWNormal = new Vector3(-1.0f, 0.0f, 0.0f);
private Vector3 signPos = new Vector3(0.0f, 1.0f, 0.0f);
private bool enableVerboseLogging = false;
private bool isCueOutOfBounds;
private bool IsCueInPlay => !isCueOutOfBounds && !ballsArePocketed[0];
private void CalculateTableCollisionConstants()
{
BALL_DIAMETER_SQUARED_MINUS_EPSILON = Mathf.Pow(ballDiameter, 2) - 0.0002f;
ballRadius = ballDiameter / 2;
ONE_OVER_BALL_RADIUS = 1 / (ballDiameter / 2);
BALL_DIAMETER_SQUARED = Mathf.Pow(ballDiameter, 2);
BALL_RADIUS_SQUARED = Mathf.Pow(ballDiameter / 2, 2);
POCKET_INNER_RADIUS_SQUARED = Mathf.Pow(pocketInnerRadius, 2);
tableWidthMinusHeight = tableWidth - tableHeight;
// Major source vertices
vA.x = pocketOuterRadius * 0.92f;
vA.z = tableHeight;
vB.x = tableWidth - pocketOuterRadius;
vB.z = tableHeight;
vC.x = tableWidth;
vC.z = tableHeight - pocketOuterRadius;
vD.x = vA.x - 0.016f;
vD.z = vA.z + 0.060f;
// Aux points
vX = vD + Vector3.forward;
vW = vC;
vW.z = 0.0f;
vY = vB;
vY.x += 1.0f;
vY.z += 1.0f;
vZ = vC;
vZ.x += 1.0f;
vZ.z += 1.0f;
// Normals
vAvD = vD - vA;
vAvD = vAvD.normalized;
vAvDNormal.x = -vAvD.z;
vAvDNormal.z = vAvD.x;
vBvY = vB - vY;
vBvY = vBvY.normalized;
vBvYNormal.x = -vBvY.z;
vBvYNormal.z = vBvY.x;
vCvZNormal = -vBvYNormal;
// Minkowski difference
pN = vA;
pN.z -= ballRadius;
pL = vD + vAvDNormal * ballRadius;
pK = vD;
pK.x -= ballRadius;
pO = vB;
pO.z -= ballRadius;
pP = vB + vBvYNormal * ballRadius;
pQ = vC + vCvZNormal * ballRadius;
pR = vC;
pR.x -= ballRadius;
pT = vX;
pT.x -= ballRadius;
pS = vW;
pS.x -= ballRadius;
pS = vW;
pS.x -= ballRadius;
}
// Update loop-scoped handler for cue-locked functionality (warming up and hitting the ball).
// Non-pure. Returns a Vector3 as it can modify the exact position the cue tip is at.
private Vector3 AimAndHitCueBall(Vector3 copyOfLocalSpacePositionOfCueTip, Vector3 cueballPosition)
{
float sweepTimeBall = Vector3.Dot(cueballPosition - localSpacePositionOfCueTipLastFrame,
cueLocalForwardDirection);
// Check for potential skips due to low frame rate
if (sweepTimeBall > 0.0f && sweepTimeBall <
(localSpacePositionOfCueTipLastFrame - copyOfLocalSpacePositionOfCueTip).magnitude)
{
copyOfLocalSpacePositionOfCueTip =
localSpacePositionOfCueTipLastFrame + cueLocalForwardDirection * sweepTimeBall;
}
// Hit condition is when cuetip is gone inside ball
if ((copyOfLocalSpacePositionOfCueTip - cueballPosition).sqrMagnitude < BALL_RADIUS_SQUARED)
{
Vector3 force = copyOfLocalSpacePositionOfCueTip - localSpacePositionOfCueTipLastFrame;
HitBallWithCue(cueTip.transform.forward, Mathf.Min(force.magnitude / Time.fixedDeltaTime, 999.0f));
}
return copyOfLocalSpacePositionOfCueTip;
}
private const float CUE_MASS = 0.5f; // kg
private const float m_e = 0.02f;
private void HitBallWithCue(Vector3 shotDirection, float velocity)
{
//shotDirection = tableSurface.rotation * shotDirection;
Vector3 q = tableSurface.InverseTransformDirection(shotDirection); // direction of cue in surface space
Vector3 o = ballTransforms[0].localPosition; // location of ball in surface
Vector3 up = tableSurface.up;
Vector3 j = -Vector3.ProjectOnPlane(q, up); // project cue direction onto table surface, gives us j
Vector3 i = Vector3.Cross(j, up);
Plane jkPlane = new Plane(i, o);
Debug.DrawLine(o, o + i, Color.red, 15f);
Vector3 Q = raySphereOutput; // point of impact in surface space
# if UNITY_EDITOR
var worldCueHitPoint = tableSurface.position + Q;
Debug.DrawLine(worldCueHitPoint, worldCueHitPoint + shotDirection, Color.magenta, 15f);
# endif
var a = jkPlane.GetDistanceToPoint(Q);
var b = Q.y - o.y;
var c = Mathf.Sqrt(Mathf.Max(0, Mathf.Pow(ballRadius, 2) - Mathf.Pow(a, 2) - Mathf.Pow(b, 2)));
var adj = Mathf.Sqrt(Mathf.Pow(q.x, 2) + Mathf.Pow(q.z, 2));
var opp = q.y;
var theta = -Mathf.Atan(opp / adj);
var cosTheta = Mathf.Cos(theta);
var sinTheta = Mathf.Sin(theta);
var f = 2 * MASS_OF_BALL * velocity / (1 + MASS_OF_BALL / CUE_MASS + 5 / (2 * ballRadius) *
(Mathf.Pow(a, 2) + Mathf.Pow(b, 2) * Mathf.Pow(cosTheta, 2) + Mathf.Pow(c, 2) * Mathf.Pow(sinTheta, 2) -
2 * b * c * cosTheta * sinTheta));
var I = 2f / 5f * MASS_OF_BALL * Mathf.Pow(ballRadius, 2);
Vector3 v = new Vector3(0, -f / MASS_OF_BALL * cosTheta, -f / MASS_OF_BALL * sinTheta);
Vector3 w = 1 / I * new Vector3(-c * f * sinTheta + b * f * cosTheta, a * f * sinTheta, -a * f * cosTheta);
// the paper is inconsistent here. either w.x is inverted (i.e. the i axis points right instead of left) or b is inverted (which means F is wrong too)
// for my sanity I'm going to assume the former
w.x = -w.x;
// https://billiards.colostate.edu/physics_articles/Alciatore_pool_physics_article.pdf
var alpha = -Mathf.Atan(
(5f / 2f * a / ballRadius * Mathf.Sqrt(Mathf.Max(0, 1f - Mathf.Pow(a / ballRadius, 2)))) /
(1 + MASS_OF_BALL / m_e + 5f / 2f * (1f - Mathf.Pow(a / ballRadius, 2)))
) * 180 / Mathf.PI;
// rewrite to the axis we expect
v = new Vector3(-v.x, v.z, -v.y);
w = new Vector3(w.x, -w.z, w.y);
if (v.y > 0)
{
// no scooping
v.y = 0;
}
else if (v.y < 0)
{
// the ball must not be under the cue after one time step
var k_MIN_HORIZONTAL_VEL = (ballRadius - c) / TIME_PER_STEP;
if (v.z < k_MIN_HORIZONTAL_VEL)
{
// not enough strength to be a jump shot
v.y = 0;
}
else
{
// dampen y velocity because the table will eat a lot of energy (we're driving the ball straight into it)
v.y = -v.y * 0.35f;
}
}
// translate
Quaternion r = Quaternion.FromToRotation(Vector3.back, j);
v = r * v;
w = r * w;
// apply squirt
v = Quaternion.AngleAxis(alpha, tableSurface.up) * v;
// done
currentBallVelocities[0] = v;
currentAngularVelocities[0] = w;
HandleCueBallHit();
}
// TODO: This is a single-use function we can refactor. Note that its use is to equate a bool,
// so it's more acceptable to hold on to.
private bool IsIntersectingWithSphere(Vector3 start, Vector3 dir, Vector3 sphere)
{
Vector3 nrm = dir.normalized;
Vector3 h = sphere - start;
float lf = Vector3.Dot(nrm, h);
float s = BALL_RADIUS_SQUARED - Vector3.Dot(h, h) + (lf * lf);
if (s < 0.0f)
{
return false;
}
s = Mathf.Sqrt(s);
if (lf < s)
{
if (lf + s >= 0)
{
s = -s;
}
else
{
return false;
}
}
raySphereOutput = start + (nrm * (lf - s));
return true;
}
/// <summary>
/// Is cue touching another ball?
/// </summary>
private bool IsCueContacting()
{
switch(gameMode) {
case GameMode.EightBall:
// Check all
for (int i = 1; i < NUMBER_OF_SIMULATED_BALLS; i++)
{
if (ballsArePocketed[i])
continue;
if ((currentBallPositions[0] - currentBallPositions[i]).sqrMagnitude < BALL_DIAMETER_SQUARED)
return true;
}
return false;
case GameMode.NineBall:
// Only check to 9 ball
for (int i = 1; i <= 9; i++)
{
if (ballsArePocketed[i])
{
continue;
}
if ((currentBallPositions[0] - currentBallPositions[i]).sqrMagnitude < BALL_DIAMETER_SQUARED)
{
return true;
}
}
return false;
case GameMode.KoreanCarom:
case GameMode.JapaneseCarom:
if ((currentBallPositions[0] - currentBallPositions[9]).sqrMagnitude < BALL_DIAMETER_SQUARED)
return true;
if ((currentBallPositions[0] - currentBallPositions[2]).sqrMagnitude < BALL_DIAMETER_SQUARED)
return true;
if ((currentBallPositions[0] - currentBallPositions[3]).sqrMagnitude < BALL_DIAMETER_SQUARED)
return true;
return false;
case GameMode.ThreeCushionCarom:
if ((currentBallPositions[0] - currentBallPositions[9]).sqrMagnitude < BALL_DIAMETER_SQUARED)
return true;
if ((currentBallPositions[0] - currentBallPositions[2]).sqrMagnitude < BALL_DIAMETER_SQUARED)
return true;
return false;
}
return false;
}
private void AdvancePhysicsStep()
{
var ballsMoving = false;
// Cue angular velocity
var moved = new bool[NUMBER_OF_SIMULATED_BALLS];
if (IsCueInPlay)
{
# if UNITY_EDITOR
var ballPos = tableSurface.position + currentBallPositions[0];
# endif
if (currentBallPositions[0].y < 0)
{
currentBallPositions[0].y = -currentBallPositions[0].y * 0.35f; // bounce with restitution
currentBallVelocities[0].y = -currentBallVelocities[0].y * 0.35f;
// Nullify small bounces
if (currentBallVelocities[0].y < 0.0025f)
{
currentBallPositions[0].y = 0;
currentBallVelocities[0].y = 0;
}
}
// Apply movement
var deltaPos = CalculateCueBallDeltaPosition();
currentBallPositions[0] += deltaPos;
moved[0] = deltaPos != Vector3.zero;
ballsMoving |= StepOneBall(0, moved);
# if UNITY_EDITOR
Debug.DrawLine(ballPos, tableSurface.position + currentBallPositions[0], Color.cyan, 15);
# endif
}
// Run main simulation / inter-ball collision
for (var i = 1; i < ballsArePocketed.Length; i++)
{
if (ballsArePocketed[i])
continue;
currentBallVelocities[i].y = 0;
currentBallPositions[i].y = 0;
var deltaPos = currentBallVelocities[i] * TIME_PER_STEP;
currentBallPositions[i] += deltaPos;
moved[i] = deltaPos != Vector3.zero;
ballsMoving |= StepOneBall(i, moved);
}
// Check if simulation has settled
if (!ballsMoving && turnIsRunning && !_preventEndOfTurn)
{
turnIsRunning = false;
// Make sure we only run this from the client who initiated the move
if (isSimulatedByUs)
HandleEndOfTurn();
// Check if there was a network update on hold
if (!isUpdateLocked)
return;
isUpdateLocked = false;
ReadNetworkData();
return;
}
if (
IsCueInPlay &&
currentBallPositions[0].y > ballRadius &&
(Mathf.Abs(currentBallPositions[0].x) > tableWidth + 0.0001f ||
Mathf.Abs(currentBallPositions[0].z) > tableHeight + 0.0001f)
)
{
HandleCueBallOffTable();
}
switch (gameMode) {
case GameMode.EightBall:
case GameMode.NineBall:
if (moved[0] && IsCueInPlay)
CalculatePoolBallPhysics(0);
// Run edge collision
for (var i = 1; i < ballsArePocketed.Length; i++)
{
if (moved[i] && !ballsArePocketed[i])
CalculatePoolBallPhysics(i);
}
if (moved[0] && IsCueInPlay)
CheckIfBallsArePocketed(0);
// Run triggers
for (var i = 1; i < ballsArePocketed.Length; i++)
{
if (moved[i] && !ballsArePocketed[i])
CheckIfBallsArePocketed(i);
}
break;
case GameMode.KoreanCarom:
case GameMode.JapaneseCarom:
if (moved[0] && IsCueInPlay)
CalculateCaromBallPhysics(0);
if (moved[9])
CalculateCaromBallPhysics(9);
if (moved[2])
CalculateCaromBallPhysics(2);
if (moved[3])
CalculateCaromBallPhysics(3);
break;
case GameMode.ThreeCushionCarom:
if (moved[0] && IsCueInPlay)
CalculateCaromBallPhysics(0);
if (moved[9])
CalculateCaromBallPhysics(9);
if (moved[2])
CalculateCaromBallPhysics(2);
break;
}
}
private Vector3 CalculateCueBallDeltaPosition()
{
if (!IsCueInPlay)
return Vector3.zero;
// Get what will be the next position
var originalDelta = currentBallVelocities[0] * TIME_PER_STEP;
var norm = currentBallVelocities[0].normalized;
// Closest found values
var minlf = float.MaxValue;
var minid = 0;
float mins = 0;
// Loop balls look for collisions
for (var i = 1; i < ballsArePocketed.Length; i++)
{
if (ballsArePocketed[i])
continue;
var h = currentBallPositions[i] - currentBallPositions[0];
var lf = Vector3.Dot(norm, h);
if (lf < 0f)
continue;
var s = BALL_DIAMETER_SQUARED_MINUS_EPSILON - Vector3.Dot(h, h) + lf * lf;
if (s < 0.0f)
continue;
if (!(lf < minlf))
continue;
minlf = lf;
minid = i;
mins = s;
}
if (minid <= 0)
return originalDelta;
var nmag = minlf - Mathf.Sqrt(mins);
// Assign new position if got appropriate magnitude
if (nmag * nmag < originalDelta.sqrMagnitude)
return norm * nmag;
return originalDelta;
}
private bool StepOneBall(int id, bool[] moved)
{
GameObject ball = ballTransforms[id].gameObject;
bool isBallMoving = false;
// no point updating velocity if ball isn't moving
if (currentBallVelocities[id] != Vector3.zero || currentAngularVelocities[id] != Vector3.zero)
{
isBallMoving = UpdateVelocity(id, ball);
}
moved[id] |= isBallMoving;
// check for collisions. a non-moving ball might be collided by a moving one
for (var i = id + 1; i < ballsArePocketed.Length; i++)
{
if (ballsArePocketed[i])
{
continue;
}
Vector3 delta = currentBallPositions[i] - currentBallPositions[id];
var dist = delta.sqrMagnitude;
if (!(dist < BALL_DIAMETER_SQUARED))
{
continue;
}
dist = Mathf.Sqrt(dist);
Vector3 normal = delta / dist;
// static resolution
Vector3 res = (ballDiameter - dist) * normal;
currentBallPositions[i] += res;
currentBallPositions[id] -= res;
moved[i] = true;
moved[id] = true;
Vector3 velocityDelta = currentBallVelocities[id] - currentBallVelocities[i];
float dot = Vector3.Dot(velocityDelta, normal);
// Dynamic resolution (Cr is assumed to be (1)+1.0)
Vector3 reflection = normal * dot;
currentBallVelocities[id] -= reflection;
currentBallVelocities[i] += reflection;
HandleBallCollision(id, i, reflection);
}
return isBallMoving;
}
private bool UpdateVelocity(int id, GameObject ball)
{
bool ballMoving = false;
// Since v1.5.0
Vector3 V = currentBallVelocities[id];
Vector3 VwithoutY = new Vector3(V.x, 0, V.z);
Vector3 W = currentAngularVelocities[id];
Vector3 cv;
// Equations derived from: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.89.4627&rep=rep1&type=pdf
//
// R: Contact location with ball and floor aka: (0,-r,0)
// µₛ: Slipping friction coefficient
// µᵣ: Rolling friction coefficient
// i: Up vector aka: (0,1,0)
// g: Planet Earth's gravitation acceleration ( 9.80665 )
//
// Relative contact velocity (marlow):
// c = v + R✕ω
//
// Ball is classified as 'rolling' or 'slipping'. Rolling is when the relative velocity is none and the ball is
// said to be in pure rolling motion
//
// When ball is classified as rolling:
// Δv = -µᵣ∙g∙Δt∙(v/|v|)
//
// Angular momentum can therefore be derived as:
// ωₓ = -vᵤ/R
// ωᵧ = 0
// ωᵤ = vₓ/R
//
// In the slipping state:
// Δω = ((-5∙µₛ∙g)/(2/R))∙Δt∙i✕(c/|c|)
// Δv = -µₛ∙g∙Δt(c/|c|)
if (currentBallPositions[id].y < 0.001)
{
// Relative contact velocity of ball and table
cv = VwithoutY + Vector3.Cross(CONTACT_POINT, W);
float cvMagnitude = cv.magnitude;
// Rolling is achieved when cv's length is approaching 0
// The epsilon is quite high here because of the fairly large timestep we are working with
if (cvMagnitude <= 0.1f)
{
//V += -k_F_ROLL * k_GRAVITY * k_FIXED_TIME_STEP * V.normalized;
// (baked):
V += -0.00122583125f * VwithoutY.normalized;
// Calculate rolling angular velocity
W.x = -V.z * ONE_OVER_BALL_RADIUS;
if (0.3f > Mathf.Abs(W.y))
{
W.y = 0.0f;
}
else
{
W.y -= Mathf.Sign(W.y) * 0.3f;
}
W.z = V.x * ONE_OVER_BALL_RADIUS;
// Stopping scenario
if (V.sqrMagnitude < 0.0001f && W.magnitude < 0.04f)
{
W = Vector3.zero;
V = Vector3.zero;
}
else
{
ballMoving = true;
}
}
else // Slipping
{
Vector3 nv = cv / cvMagnitude;
// Angular slipping friction
//W += ((-5.0f * k_F_SLIDE * k_GRAVITY)/(2.0f * 0.03f)) * k_FIXED_TIME_STEP * Vector3.Cross( Vector3.up, nv );
// (baked):
W += -2.04305208f * Vector3.Cross(Vector3.up, nv);
//V += -k_F_SLIDE * k_GRAVITY * k_FIXED_TIME_STEP * nv;
// (baked):
V += -0.024516625f * nv;
ballMoving = true;
}
}
else
{
ballMoving = true;
}
if (currentBallPositions[id].y > 0) // small epsilon to apply gravity
V.y -= EARTH_GRAVITY * TIME_PER_STEP;
else
V.y = 0;
currentAngularVelocities[id] = W;
currentBallVelocities[id] = V;
ball.transform.Rotate(this.transform.TransformDirection(W.normalized),
W.magnitude * TIME_PER_STEP * -Mathf.Rad2Deg, Space.World);
return ballMoving;
}
private void CalculatePoolBallPhysics(int id)
{
Vector3 nToVNormalized;
Vector3 a_to_v;
float dot;
var ballUnderTest = currentBallPositions[id];
signPos.x = Mathf.Sign(ballUnderTest.x);
signPos.z = Mathf.Sign(ballUnderTest.z);
ballUnderTest = Vector3.Scale(ballUnderTest, signPos);
if (ballUnderTest.x > vA.x) // Major Regions
{
if (ballUnderTest.x > ballUnderTest.z + tableWidthMinusHeight) // Minor B
{
if (ballUnderTest.z < tableHeight - pocketOuterRadius)
{
// Region H
if (ballUnderTest.x > tableWidth - ballRadius)
{
// Static resolution
ballUnderTest.x = tableWidth - ballRadius;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(vCvWNormal, signPos));
}
}
else
{
a_to_v = ballUnderTest - vC;
if (Vector3.Dot(a_to_v, vBvY) > 0.0f)
{
// Region I ( VORONI )
if (a_to_v.magnitude < ballRadius)
{
// Static resolution
nToVNormalized = a_to_v.normalized;
ballUnderTest = vC + nToVNormalized * ballRadius;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(nToVNormalized, signPos));
}
}
else
{
// Region J
a_to_v = ballUnderTest - pQ;
if (Vector3.Dot(vCvZNormal, a_to_v) < 0.0f)
{
// Static resolution
dot = Vector3.Dot(a_to_v, vBvY);
ballUnderTest = pQ + dot * vBvY;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(vCvZNormal, signPos));
}
}
}
}
else // Minor A
{
if (ballUnderTest.x < vB.x)
{
// Region A
if (ballUnderTest.z > pN.z)
{
// Velocity based A->C delegation ( scuffed CCD )
a_to_v = ballUnderTest - vA;
var _V = Vector3.Scale(currentBallVelocities[id], signPos);
var V = new Vector3(-_V.z, 0.0f, _V.x);
if (ballUnderTest.z > vA.z)
{
if (Vector3.Dot(V, a_to_v) > 0.0f)
{
// Region C ( Delegated )
a_to_v = ballUnderTest - pL;
// Static resolution
dot = Vector3.Dot(a_to_v, vAvD);
ballUnderTest = pL + dot * vAvD;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(vAvDNormal, signPos));
}
else
{
// Static resolution
ballUnderTest.z = pN.z;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(vAvBNormal, signPos));
}
}
else
{
// Static resolution
ballUnderTest.z = pN.z;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(vAvBNormal, signPos));
}
}
}
else
{
a_to_v = ballUnderTest - vB;
if (Vector3.Dot(a_to_v, vBvY) > 0.0f)
{
// Region F ( VERONI )
if (a_to_v.magnitude < ballRadius)
{
// Static resolution
nToVNormalized = a_to_v.normalized;
ballUnderTest = vB + nToVNormalized * ballRadius;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(nToVNormalized, signPos));
}
}
else
{
// Region G
a_to_v = ballUnderTest - pP;
if (Vector3.Dot(vBvYNormal, a_to_v) < 0.0f)
{
// Static resolution
dot = Vector3.Dot(a_to_v, vBvY);
ballUnderTest = pP + dot * vBvY;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(vBvYNormal, signPos));
}
}
}
}
}
else
{
a_to_v = ballUnderTest - vA;
if (Vector3.Dot(a_to_v, vAvD) > 0.0f)
{
a_to_v = ballUnderTest - vD;
if (Vector3.Dot(a_to_v, vAvD) > 0.0f)
{
if (ballUnderTest.z > pK.z)
{
// Region E
if (ballUnderTest.x > pK.x)
{
// Static resolution
ballUnderTest.x = pK.x;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(vCvWNormal, signPos));
}
}
else
{
// Region D ( VORONI )
if (a_to_v.magnitude < ballRadius)
{
// Static resolution
nToVNormalized = a_to_v.normalized;
ballUnderTest = vD + nToVNormalized * ballRadius;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(nToVNormalized, signPos));
}
}
}
else
{
// Region C
a_to_v = ballUnderTest - pL;
if (Vector3.Dot(vAvDNormal, a_to_v) < 0.0f)
{
// Static resolution
dot = Vector3.Dot(a_to_v, vAvD);
ballUnderTest = pL + dot * vAvD;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(vAvDNormal, signPos));
}
}
}
else
{
// Region B ( VORONI )
if (a_to_v.magnitude < ballRadius)
{
// Static resolution
nToVNormalized = a_to_v.normalized;
ballUnderTest = vA + nToVNormalized * ballRadius;
// Dynamic
ApplyCushionBounce(id, Vector3.Scale(nToVNormalized, signPos));
}
}
}
currentBallPositions[id] = Vector3.Scale(ballUnderTest, signPos);
}
private void ApplyCushionBounce(int id, Vector3 n)
{
// Don't bounce if the cushions obviously could not collide.
if (currentBallPositions[id].y > ballRadius)
return;
// Reject bounce if velocity is going the same way as normal
// this state means we tunneled, but it happens only on the corner
// vertexes
Vector3 currentBallVelocity = currentBallVelocities[id];
if (Vector3.Dot(currentBallVelocity, n) > 0.0f)
return;
// Rotate V, W to be in the reference frame of cushion
Quaternion rq = Quaternion.AngleAxis(Mathf.Atan2(-n.z, -n.x) * Mathf.Rad2Deg, Vector3.up);
Quaternion rb = Quaternion.Inverse(rq);
Vector3 V = rq * currentBallVelocity;
Vector3 W = rq * currentAngularVelocities[id];
Vector3 V1 = new Vector3(-V.x * F - 0.00240675711f * W.z, 0f,
0.71428571428f * V.z + 0.00857142857f * (W.x * SIN_A - W.y * COS_A) - V.z);
var s_x = V.x * SIN_A + W.z;
var s_z = -V.z - W.y * COS_A + W.x * SIN_A;
var k = s_z * 0.71428571428f;
var c = V.x * COS_A;
Vector3 W1 = new Vector3(k * SIN_A, k * COS_A, 15.625f * (-s_x * 0.04571428571f + c * 0.0546021744f));
// Unrotate result
currentBallVelocities[id] += rb * V1;
currentAngularVelocities[id] += rb * W1;
if (id == 0)
cushionsHitThisTurn++;
}
private void CheckIfBallsArePocketed(int id)
{
Vector3 A = currentBallPositions[id];
Vector3 absA = new Vector3(Mathf.Abs(A.x), A.y, Mathf.Abs(A.z));
if (
(absA - cornerPocket).sqrMagnitude < POCKET_INNER_RADIUS_SQUARED ||
(absA - middlePocket).sqrMagnitude < POCKET_INNER_RADIUS_SQUARED ||
absA.z > middlePocket.z ||
absA.z > -absA.x + cornerPocket.x + cornerPocket.z
)
{
TriggerPocketBall(id);
currentBallVelocities[id] = Vector3.zero;
currentAngularVelocities[id] = Vector3.zero;
}
}
private bool _preventEndOfTurn;
private void HandleCueBallOffTable()
{
if (logger)
logger._Log(name, "HandleCueBallOffTable");
// Only run this once.
if (!IsCueInPlay)
return;
isCueOutOfBounds = true;
if (IsCarom)
fourBallCueLeftTable = true;
else
ballsArePocketed[0] = true;
HandleFoulEffects();
// VFX ( make ball move )
var body = ballRigidbodies[0];
body.isKinematic = false;
body.velocity = transform.TransformVector(new Vector3(
currentBallVelocities[0].x,
currentBallVelocities[0].y,
currentBallVelocities[0].z
));
if (!cueBallController)
return;
cueBallController._EnableDonking(this, 3);
_preventEndOfTurn = true;
}
public void _AllowEndOfTurn()
{
_preventEndOfTurn = false;
}
private void TriggerPocketBall(int id)
{
var total = 0;
// Get total for X positioning
foreach (var pocketed in ballsArePocketed)
{
if (pocketed)
total++;
}
// place ball on the rack
currentBallPositions[id] = Vector3.zero + (float) total * ballDiameter * Vector3.zero;
// This is where we actually save the pocketed/non-pocketed state of balls.
ballsArePocketed[id] = true;
bool success = false;
// isOpen is only ever false in blackball - so this covers any ball sunk in 9 ball, which is correct (in
// our simplified 9-ball, the only way to trigger a foul is to foul the cue ball or not hit the lowest-count
// ball first when shooting.
if (isOpen && id > 1)
success = true;
// it is blue's turn
else if ((isTeam2Turn && isTeam2Blue || !isTeam2Turn && !isTeam2Blue) && id > 1 && id < 9)
success = true;
// it is orange's turn
else if (id >= 9)
success = true;
HandleBallSunk(success);
if (id == 0)
isCueOutOfBounds = true;
// VFX ( make ball move )
var body = ballRigidbodies[id];
body.isKinematic = false;
body.velocity = transform.TransformVector(new Vector3(
currentBallVelocities[id].x,
currentBallVelocities[id].y,
currentBallVelocities[id].z
));
}
}
}